[MKDoc-dev] Events Component: Milestone 3 Complete

Sam Tregar sam at tregar.com
Wed Nov 3 20:52:17 GMT 2004


Please find a patch implementing the final milestone of the events
component project, newletter support for events.  This is a patch
against the mkdoc-1-6 branch which has some of the code from the first
milestone already committed.

I still have one bug to fix in the TimeRange component code concerning
deleting from the Document_TimeRange table when time-ranges are
removed from a document, which I will work on later this week.  Aside
from that I believe that all the goals of the project have been met.
Please let me know if you find any problems or have any questions.

Unless other bugs come up I plan to spend the remainder of the time
allocated on the project (around 8 hours) writing a document
describing how to add new document components to MKDoc.  This will
obviously be of use to future developers but it will also serve to
expand my understanding of what I just did and double-check my
assumptions.

-sam
-------------- next part --------------
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/flo/editor/Headlines.pm mkd/flo/editor/Headlines.pm
--- mkd-1.6/flo/editor/Headlines.pm	2004-09-16 05:55:45.000000000 -0400
+++ mkd/flo/editor/Headlines.pm	2004-11-02 22:28:09.000000000 -0500
@@ -1,7 +1,7 @@
 # -------------------------------------------------------------------------------------
 # flo::editor::Headlines
 # -------------------------------------------------------------------------------------
-# Author : Jean-Michel Hiver.
+# Author : Jean-Michel Hiver, Sam Tregar
 # Copyright : (c) MKDoc Holdings Ltd, 2003
 #
 # This file is part of MKDoc. 
@@ -52,9 +52,31 @@
     $self->{title}         = $args->{title}           || '';
     $self->{from_path}     = $args->{'from_path'}     || '';
     $self->{max_headlines} = $args->{'max_headlines'} || '';
-    $self->{leaf_only}     = $args->{'leaf_only'} || '';
+    $self->{leaf_only}     = $args->{'leaf_only'}     || '';
+    $self->{mode}          = $args->{'mode'}          || 'newest';
 }
 
+# getter for mode
+sub mode {
+    my $self = shift;
+    croak("Too many args for getter!") if @_;
+    return $self->{mode};
+}
+
+# true when mode == newest
+sub mode_is_newest { shift->{mode} eq 'newest' }
+
+# true when mode == upcoming
+sub mode_is_upcoming { shift->{mode} eq 'upcoming' }
+
+# setter for mode, must be newest or upcoming
+sub set_mode {
+    my ($self, $value) = @_;
+    croak("Wrong number of args for setter!") if @_ != 2;
+    croak("Mode must be 'newest' or 'upcoming' not '$value'.") 
+      unless $value eq 'newest' or $value eq 'upcoming';
+    $self->{mode} = $value;
+}
 
 sub leaf_only
 {
@@ -73,9 +95,20 @@
     
     return $self->validate_title() &
            $self->validate_from_path() &
-	   $self->validate_max_headlines();
+	   $self->validate_max_headlines() &
+           $self->validate_mode;
 }
 
+# check mode value
+sub validate_mode {
+    my $self = shift;
+    unless (defined $self->{mode} and 
+            ($self->{mode} eq 'newest' or $self->{mode} eq 'upcoming')) {
+	new MKDoc::Ouch 'component/headlines/mode_invalid';
+	return 0;
+    }
+    return 1;
+}
 
 sub validate_title
 {
@@ -210,7 +243,6 @@
         } @path;
 
         my $path = join '|', @path;
-        warn $path;
         return "^($path)\$";
     }
     else
@@ -264,7 +296,80 @@
 sub default_headlines
 {
     my $self = shift;
+    my $mode = $self->{mode};
+
+    # switch on mode
+    if ($mode eq 'newest') {
+        return $self->_default_headlines_newest();
+    } else {
+        return $self->_default_headlines_upcoming();
+    }
+}
+
+sub _default_headlines_upcoming {
+    my $self = shift;
+    my $limit = $self->max_default_headlines();
+
+    # as far as I can tell the lib::sql system can't handle a simple
+    # join, so this has to be done in straight SQL.  Sigh.
+    my $sql = "SELECT Document.ID as ID, 
+                      Document_TimeRange.ID as Document_TimeRange_ID
+               FROM Document, Document_TimeRange
+               WHERE Document.ID = Document_TimeRange.Document_ID 
+                 AND Document_TimeRange.FromDate >= NOW()
+                 AND Full_Path REGEXP ?
+               ORDER BY Document_TimeRange.FromDate ASC";
+    
+    my $dbh = lib::sql::DBH->get;
+    my $sth = $dbh->prepare_cached($sql);
+    $sth->execute($self->from_path_search_value());
+    my $query = new lib::sql::Query (sth => $sth, bless_into => 'flo::Record::Document');
+    my @res = $query->fetch_all;
+    
+    my $doc_t = flo::Standard::table ('Document');
+
+    # get document objects for results
+    my @documents = map { $doc_t->get ( $_->{ID} ) } @res;
+    
+    # get timerange objects from documents
+    my @timerange_ids = map { $_->{Document_TimeRange_ID} } @res;
+    my @timeranges;
+    foreach my $doc (@documents) {
+        my $timerange_id = shift @timerange_ids;
+        # find this timerange in the list of components for this doc
+        my ($timerange) = 
+          grep { $_->isa('flo::editor::TimeRange') and
+                 $_->Document_TimeRange_ID == $timerange_id }
+            $doc->components;
+        push(@timeranges, $timerange);
+    }
+
+    # combine documents and their timeranges into a single results
+    # stream, weeding out missing timeranges.  This can happen when
+    # the Document_TimeRange table gets out of sync with the Document
+    # table
+    @res = grep { defined $_->{timerange} }
+           map  { { document  => $documents[$_],
+                    timerange => $timeranges[$_] } } (0 .. $#documents);
+                   
+    # limit to documents which are showable
+    @res = grep { $_->{document}->is_showable() } @res;
     
+    # limit to leaves
+    if ($self->leaf_only()) {
+        @res = map { my @children = $_->{document}->children_showable(); @children ? () : $_ } @res;
+    }
+ 
+    # limit to max number of headlines
+    @res = splice @res, 0, $self->max_default_headlines();
+
+    return wantarray ? @res : \@res;
+}
+
+
+sub _default_headlines_newest {
+    my $self = shift;
+
     # performs the query and put results in the stash
     my $document_t = flo::Standard::table ('Document');
     
@@ -317,15 +422,33 @@
 sub personalized_headlines
 {
     my $self = shift;
-    
     my $user = flo::Standard::current_user() || return [];
-    
+    my $mode = $self->{mode};
+
+    # concoct SQL needed for upcoming or newest mode
+    my ($extra_from, $extra_where, $extra_select, $order_by, $group_by);
+    if ($mode eq 'upcoming') {
+        $order_by     = "Document_TimeRange.FromDate ASC";
+        $group_by     = "Document_TimeRange.ID";
+
+        $extra_from   = ", Document_TimeRange";
+        $extra_where  = "AND Document.ID = Document_TimeRange.Document_ID";
+        $extra_select = ", Document_TimeRange.ID as Document_TimeRange_ID";
+    } else {
+        $order_by     = "Date_Created DESC";
+        $group_by     = "Document.ID";
+        ($extra_from, $extra_where, $extra_select) = ("") x 3;
+    }
+
+
     $self->{_personalized_headlines} ||= do {
 	# This is horrible, but I really don't see how to get around it
 	# until we get some kind of RSS search engine
 	my $sql  = <<SQL;
-SELECT Document.ID AS ID, SUM(Preference_Audience.Value) AS Pref_Score
+SELECT Document.ID AS ID, SUM(Preference_Audience.Value) AS Pref_Score 
+       $extra_select
 FROM   Document, Document_Audience, Audience, Preference_Audience, Editor, Preference_Language
+       $extra_from
 WHERE
         -- join the tables together
         (
@@ -345,8 +468,12 @@
 AND
         -- remove languages which are not wanted
         Preference_Language.Value = 1
-GROUP BY (Document.ID)
-ORDER BY Date_Created DESC
+
+$extra_where
+
+GROUP BY $group_by
+HAVING Pref_Score > 0
+ORDER BY $order_by
 SQL
 	# $sql .= "\nLIMIT 0, " . $self->max_personalized_headlines;
 	my $dbh = lib::sql::DBH->get;
@@ -355,25 +482,56 @@
 	my $query = new lib::sql::Query (sth => $sth, bless_into => 'flo::Record::Document');
 	my @res = $query->fetch_all;
 
+        # get documents from ID list
         my $doc_t = flo::Standard::table ('Document');
-        @res = map { $doc_t->get ( $_ ) }
-               map { $_->{Pref_Score} > 0 ? $_->{ID} : () }
-               @res;
-	
-	# limit to documents which are showable
-	@res = map { $_->is_showable() ? $_ : () } @res;
-	
-        if ($self->leaf_only())
-        {
-           @res = map { my @children = $_->children_showable(); @children ? () : $_ } @res;
+        my @documents = map { $doc_t->get( $_->{ID} ) } @res;
+
+        # deal with timerange data for upcoming events list
+        if ($mode eq 'upcoming') {
+            # get timerange objects from documents
+            my @timerange_ids = map { $_->{Document_TimeRange_ID} } @res;
+            my @timeranges;
+            foreach my $doc (@documents) {
+                my $timerange_id = shift @timerange_ids;
+                # find this timerange in the list of components for this doc
+                my ($timerange) = 
+                  grep { $_->isa('flo::editor::TimeRange') and
+                           $_->Document_TimeRange_ID == $timerange_id }
+                    $doc->components;
+                push(@timeranges, $timerange);
+            }
+            
+            # combine documents and their timeranges into a single
+            # results stream, weeding out missing timeranges.  This
+            # can happen when the Document_TimeRange table gets out of
+            # sync with the Document table
+            @res = grep { defined $_->{timerange} }
+                   map  { { document  => $documents[$_],
+                            timerange => $timeranges[$_] } } (0..$#documents);
+
+        } else {
+            @res = @documents;
         }
- 
-	# limit to max number of headlines
-	@res = splice @res, 0, $self->max_personalized_headlines();
-	
+
+        # limit to documents which are showable
+        @res = grep { ($mode eq 'newest' ? $_->is_showable() : 
+                                           $_->{document}->is_showable) } 
+          @res;
+        
+        if ($self->leaf_only()) {
+            @res = grep { my @children = ($mode eq 'newest' ? 
+                                         $_->children_showable() : 
+                                         $_->{document}->children_showable());
+                          @children } @res;
+        }
+        
+        # limit to max number of headlines
+        @res = splice @res, 0, $self->max_personalized_headlines();
+
 	\@res;
     };
-    
+
+                       
     my $res = $self->{_personalized_headlines};
     return wantarray ? @{$res} : $res;
 }
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/flo/editor/TimeRange.pm mkd/flo/editor/TimeRange.pm
--- mkd-1.6/flo/editor/TimeRange.pm	1969-12-31 19:00:00.000000000 -0500
+++ mkd/flo/editor/TimeRange.pm	2004-10-23 19:58:21.000000000 -0400
@@ -0,0 +1,533 @@
+# -------------------------------------------------------------------------------------
+# flo::editor::TimeRange
+# -------------------------------------------------------------------------------------
+# Author : Sam Tregar
+# Copyright : (c) MKDoc Holdings Ltd, 2004
+#
+# This file is part of MKDoc. 
+# 
+# MKDoc is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+# 
+# MKDoc is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with MKDoc; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# -------------------------------------------------------------------------------------
+package flo::editor::TimeRange;
+
+=head1 NAME
+
+flo::editor::TimeRange - an editor for time-ranges
+
+=head1 DESCRIPTION
+
+The module provides an editor for time-range components.  A time-range
+component has a title, a start date and an end date.  Documents with
+time-ranges will appear in the upcoming-events list in headlines and
+newsletters.
+
+=head1 INTERFACE
+
+=over 4
+
+=cut
+
+use flo::Standard;
+use Carp qw(croak);
+use strict;
+use warnings;
+use utf8;
+
+use flo::Record::Editor;
+use Text::Unidecode;
+use MKDoc::Config;
+use flo::Editor;
+use flo::Standard;
+use MKDoc::CGI;
+use MKDoc::Util::Text2HTML;
+use flo::RedirectManager;
+use DateTime;
+use DateTime::TimeZone;
+
+use base qw /flo::Component
+	     flo::editor::Mixin::compute_name
+	     flo::editor::Mixin::normalize_name/;
+
+use constant DATETIME_PARTS => qw(month day year hour minute tz);
+use constant DATE_PARTS     => qw(month day year);
+
+sub preferred_extension { 'html' };
+
+sub _initialize {
+    my $self = shift;
+    my $args = $self->cgi_args();
+
+    # setup simple fields
+    $self->{Document_TimeRange_ID} = $args->{Document_TimeRange_ID} || undef;
+    $self->{title}                 = $args->{title}                 || '';
+
+    # setup date fields, defaulting date parts to now and time to 00:00
+    my $now = DateTime->now(time_zone => "local");
+    foreach my $prefix (qw(from to)) {
+        foreach my $part (DATE_PARTS) {
+            $self->{"${prefix}_$part"} = $args->{"${prefix}_$part"} || $now->$part;
+        }
+        $self->{"${prefix}_hour"}   = $args->{"${prefix}_hour"}  || "00";
+        $self->{"${prefix}_minute"} = $args->{"${prefix}_minute"}|| "00";
+        $self->{"${prefix}_tz"}     = $args->{"${prefix}_tz"}    || $now->time_zone->name;
+    }
+}
+
+=item C<< $self->validate() >>
+
+Check the data entered for validity.  Makes sure everything has a
+value and that the dates aren't invalid.  Returns 1 if everything is
+ok, 0 otherwise.  When errors are found MKDoc::Ouch objects are
+created with codes to describe the problem (ex:
+component/timerange/title_empty).
+
+=cut
+
+sub validate {
+    my $self = shift;
+    my $problems = 0;
+    
+    # set up the callback for errors
+    local $MKDoc::Ouch::CALLBACK;
+    $MKDoc::Ouch::CALLBACK = sub { $self->add_error (@_) };
+
+    # look at dates individually
+    foreach my $prefix (qw(from to)) {
+        $problems += not $self->validate_date($prefix);
+    }
+
+    # compare the dates and make sure from is before to.  Only do this
+    # if everything else is ok since it won't work on incomplete dates
+    if (not $problems) {
+        my $cmp = $self->from_datetime <=> $self->to_datetime;
+        if ($cmp == 0) {
+            new MKDoc::Ouch "component/timerange/dates_equal";
+            $problems++;
+        } elsif ($cmp == 1) {
+            new MKDoc::Ouch "component/timerange/dates_reversed";
+            $problems++;
+        }
+    }                           
+
+    # validate title, must have something aside from space
+    if (not defined $self->{title} or $self->{title} =~ /^\s*$/) {
+	new MKDoc::Ouch 'component/timerange/title_empty';
+        $problems++;
+    }    
+
+    return not $problems;
+}
+
+sub validate_date {
+    my ($self, $prefix) = @_;
+    my $problems = 0;
+
+    # these must be non-zero
+    foreach my $part (qw(month day year)) {
+        next if $self->{"${prefix}_$part"};
+        new MKDoc::Ouch "component/timerange/${prefix}_${part}_empty";
+        $problems++;
+    }
+
+    # these just need to be defined
+    foreach my $part (qw(hour minute tz)) {
+        next if defined $self->{"${prefix}_$part"};
+        new MKDoc::Ouch "component/timerange/${prefix}_${part}_empty";
+        $problems++;
+    }
+
+    # make sure the date is valid if everything else was ok
+    if (not $problems and not $self->_gen_datetime($prefix)) {
+        new MKDoc::Ouch "component/timerange/${prefix}_invalid";
+        $problems++;
+    }
+
+    return not $problems;
+}
+
+=item C<< $title = $self->title >>
+
+Returns the title attribute
+
+=cut
+
+sub title { 
+    my $self = shift;
+    croak("Too many args for getter!") if @_;
+    $self->{title};
+}
+
+=item C<< $self->set_title($new_title); >>
+
+Sets the title attribute
+
+=cut
+
+sub set_title {
+    my $self = shift;
+    $self->{title} = shift;
+    $self->{title} = join ' ', split /(?:\n|\r)/, $self->{title};
+    $self->{title} =~ s/^\s+//;
+    $self->{title} =~ s/\s+$//;
+}
+
+=item C<< $id = $self->Document_TimeRange_ID >>
+
+Returns the ID for the row in Document_TimeRange corresponding to this
+object or undef if no row exists.
+
+=cut
+
+sub Document_TimeRange_ID { 
+    my $self = shift;
+    croak("Too many args for getter!") if @_;
+    $self->{Document_TimeRange_ID};
+}
+
+=item C<< $self->set_Document_TimeRange_ID($new_ID); >>
+
+Sets the ID for the row in Document_TimeRange corresponding to this
+object.
+
+=cut
+
+sub set_Document_TimeRange_ID {
+    my ($self, $value) = @_;
+    croak("Wrong number of args for setter!") if @_ != 2;
+    $self->{Document_TimeRange_ID} = $value;
+}
+
+=item C<< $month = $self->from_month >>
+
+=item C<< $self->set_from_month($month) >>
+
+=item C<< $day = $self->from_day >>
+
+=item C<< $self->set_from_day($day) >>
+
+=item C<< $year = $self->from_year >>
+
+=item C<< $self->set_from_year($year) >>
+
+=item C<< $hour = $self->from_hour >>
+
+=item C<< $self->set_from_hour($hour) >>
+
+=item C<< $minute = $self->from_minute >>
+
+=item C<< $self->set_from_minute($minute) >>
+
+=item C<< $tz = $self->from_tz >>
+
+=item C<< $self->set_from_tz($tz) >>
+
+Get/set parts of the from date.  Months are numbered from 1 to 12,
+hours are 0 to 23 and timezones are offsets from UTC in +/-HH:MM format
+(ex. +05:00, -13:50).
+
+=item C<< $month = $self->to_month >>
+
+=item C<< $self->set_to_month($month) >>
+
+=item C<< $day = $self->to_day >>
+
+=item C<< $self->set_to_day($day) >>
+
+=item C<< $year = $self->to_year >>
+
+=item C<< $self->set_to_year($year) >>
+
+=item C<< $hour = $self->to_hour >>
+
+=item C<< $self->set_to_hour($hour) >>
+
+=item C<< $minute = $self->to_minute >>
+
+=item C<< $self->set_to_minute($minute) >>
+
+=item C<< $tz = $self->to_tz >>
+
+=item C<< $self->set_to_tz($tz) >>
+
+Get/set parts of the to date.  Months are numbered from 1 to 12, hours
+are 0 to 23 and timezones are offsets from UTC in +/-HH:MM format
+(ex. +05:00, -13:50).
+
+=item C<< $date = $self->from_datetime >>
+
+Returns a DateTime object representing the from_date.  Returns undef
+if the date is invalid.
+
+=item C<< $date = $self->to_datetime >>
+
+Returns a DateTime object representing the to_date.  Returns undef if
+the date is invalid.
+
+=cut
+
+# auto-generate accessors and mutators for date parts
+BEGIN {
+    no strict 'refs';
+    foreach my $prefix qw(from to) {
+        foreach my $part (DATETIME_PARTS) {
+            my $name = "${prefix}_$part";
+            *{$name} = sub {
+                my $self = shift;
+                croak("Too many args for getter!") if @_;
+                return $self->{$name};
+            };
+            *{"set_$name"} = sub { 
+                my ($self, $value) = @_;
+                croak("Wrong number of args for setter!") if @_ != 2;
+                $self->{$name} = $value;
+            }
+        }
+    }
+}
+
+# from_datetime and to_datetime share a common worker, _gen_datetime
+sub from_datetime {
+    return shift->_gen_datetime("from");
+}
+
+sub to_datetime {
+    return shift->_gen_datetime("to");
+}
+
+sub _gen_datetime {
+    my ($self, $which) = @_;
+
+    # catch error for bad dates and just return undef
+    my $dt;
+    eval {
+        $dt = DateTime->new( (map { ($_ => $self->{"${which}_$_"}) }
+                              (qw(year month day hour minute))),
+                             second => 0,
+                             time_zone => $self->{"${which}_tz"},
+                           );
+    };
+
+    return $dt;
+}
+
+=item C<< $self->month_select($which) >>
+
+Returns an array of hashes for use in building the month selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+
+sub month_select {
+    my $self = shift;
+    croak("Wrong number of params for month_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    my $x = 0;
+    return [ map { $x++; 
+                   { label       => $_,
+                     value       => $x, 
+                     is_selected => (($self->{"${which}_month"} || 0) == $x) ? 
+                                    "selected" : undef
+                   } } 
+             (qw(January February March April May June July August September October November December)) ];
+}
+
+=item C<< $self->day_select($which) >>
+
+Returns an array of hashes for use in building the day selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+
+sub day_select {
+    my $self = shift;
+    croak("Wrong number of params for day_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    return [ map { { label       => $_,
+                     value       => $_, 
+                     is_selected => (($self->{"${which}_day"} || 0) == $_) ? 
+                                    "selected" : undef
+                   } } 
+             (1 .. 31) ];
+}
+
+=item C<< $self->year_select($which) >>
+
+Returns an array of hashes for use in building the year selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+ 
+sub year_select {
+    my $self = shift;
+    croak("Wrong number of params for year_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    my $year = (localtime())[5] + 1900;
+    $year = $self->{"${which}_year"} if $self->{"${which}_year"} and 
+                                        $year > ($self->{"${which}_year"});
+    return [ map { { label       => $_,
+                     value       => $_, 
+                     is_selected => (($self->{"${which}_year"} || 0) == $_) ? 
+                                    "selected" : undef
+                   } } 
+             ($year .. $year + 25) ];
+}
+
+=item C<< $self->hour_select($which) >>
+
+Returns an array of hashes for use in building the hour selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+ 
+sub hour_select {
+    my $self = shift;
+    croak("Wrong number of params for hour_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    return [ map { { label       => sprintf("%02d", $_),
+                     value       => $_, 
+                     is_selected => (($self->{"${which}_hour"} || 0) == $_) ? 
+                                    "selected" : undef
+                   } } 
+             (0 .. 23) ];
+}
+
+=item C<< $self->minute_select($which) >>
+
+Returns an array of hashes for use in building the minute selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+ 
+sub minute_select {
+    my $self = shift;
+    croak("Wrong number of params for minute_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    return [ map { { label       => sprintf("%02d", $_),
+                     value       => $_, 
+                     is_selected => (($self->{"${which}_minute"} || 0) == $_) ? 
+                                    "selected" : undef
+                   } } 
+             (0 .. 59) ];
+}
+
+=item C<< $self->tz_select($which) >>
+
+Returns an array of hashes for use in building the tz selector in
+the time-range editor.  Pass "from" or "to" as the sole parameter
+depending on which selector is being built.  Keys are 'label', 'value'
+and 'is_selected'.
+
+=cut
+
+# I wish there was a way to do this that used documented
+# functionality.  Maybe we'll switch to DateTime::TimeZone.  Also, the
+# list of timezones is much too small.
+ 
+sub tz_select {
+    my $self = shift;
+    croak("Wrong number of params for tz_select().") unless @_ == 1;
+    my $which = shift;
+    croak("Bad value for which: '$which'.  Should be 'from' or 'to'.")
+      unless $which eq 'from' or $which eq 'to';
+    my $value = $self->{"${which}_tz"};
+
+    # return all available names with current selected
+    return [ map { { label       => $_,
+                     value       => $_,
+                     is_selected => $_ eq $value ? 1 : undef,
+                 } } DateTime::TimeZone->all_names ];
+}
+
+=item C<< $self->generate_xml() >>
+
+This class overrides generate_xml() in order to update the
+Document_TimeRange table when the user submits a change.
+
+=cut
+
+# it might be better to have an explicit on_save() hook available,
+# since it's possible that something else might want to use
+# generate_xml() someday.  At the moment it appears to be used only to
+# save changes to the database making it acceptable for this use.
+
+sub generate_xml {
+    my $self = shift;
+    $self->write_index();
+    return $self->SUPER::generate_xml(@_);
+}
+
+=item C<< $self->write_index() >>
+
+Called by generate_xml() to write to the index table,
+Document_TimeRange.  Returns 1 on success.
+
+=cut
+
+sub write_index {
+    my $self = shift;
+    my $dbh = lib::sql::DBH->get();
+    my $id = $self->Document_TimeRange_ID;
+
+    # normalize the dates to UTC to make searches easier and format
+    # them for MySQL
+    my ($from_date, $to_date) =
+      map { my $date = $self->_gen_datetime($_);
+            $date->set_time_zone("UTC");
+            $date->strftime('%Y-%m-%d %H:%M:00') }
+        (qw(from to));
+
+    if ($id) {
+        # if an ID already exists do an UPDATE
+        $dbh->do('UPDATE Document_TimeRange 
+                  SET Document_ID = ?, FromDate = ?, ToDate = ?
+                  WHERE ID = ?', undef, $self->parent_id, 
+                 $from_date, $to_date, $id);
+    } else {
+        # insert a new record and get back the ID
+        $dbh->do('INSERT INTO Document_TimeRange 
+                  (Document_ID, FromDate, ToDate) VALUES (?, ?, ?)',
+                  undef, $self->parent_id, $from_date, $to_date);
+        $self->set_Document_TimeRange_ID($dbh->{mysql_insertid});
+    }
+
+    return 1;
+}
+
+=back
+
+=cut
+
+1;
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/INSTALL.TXT mkd/INSTALL.TXT
--- mkd-1.6/INSTALL.TXT	2004-09-30 10:49:26.000000000 -0400
+++ mkd/INSTALL.TXT	2004-10-19 12:18:27.000000000 -0400
@@ -160,7 +160,8 @@
     MKDoc::XML
     MKDoc::Control_List
     MKDoc::Text::Structured
-
+    DateTime
+    DateTime::TimeZone
 
 Configuring your MKDoc system
 =============================
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/lib/sql/DBH.pm mkd/lib/sql/DBH.pm
--- mkd-1.6/lib/sql/DBH.pm	2003-06-02 05:28:05.000000000 -0400
+++ mkd/lib/sql/DBH.pm	2004-10-23 13:33:58.000000000 -0400
@@ -59,7 +59,7 @@
     my $self = shift;
     if (not defined $self or not ref $self) { $self = _ETERNAL_ }
     defined $self or
-	confess "Cannot return \$dbh because $self has not been spawned";
+	confess "Cannot return \$dbh because \$self has not been spawned";
     
     defined _DBH_ and return _DBH_;
     
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/MKDoc/Site/Deploy/DB/Schema.pm mkd/MKDoc/Site/Deploy/DB/Schema.pm
--- mkd-1.6/MKDoc/Site/Deploy/DB/Schema.pm	2004-09-16 05:55:44.000000000 -0400
+++ mkd/MKDoc/Site/Deploy/DB/Schema.pm	2004-10-16 18:04:37.000000000 -0400
@@ -341,6 +341,29 @@
   );
 
 
+## DOCUMENT TIMERANGE TABLE ##
+new lib::sql::Table
+  (
+   name => 'Document_TimeRange',
+   pk   => [ qw /ID/ ],
+   ai   => 1,
+   cols => [
+	    { name => 'ID',
+              type => new lib::sql::type::Int( not_null => 1 ), },
+	    { name => 'Document_ID',
+              type => new lib::sql::type::Int( not_null => 1 ), },
+	    { name => 'FromDate',
+              type => new lib::sql::type::DateTime( not_null => 1 ), },
+	    { name => 'ToDate',
+              type => new lib::sql::type::DateTime( not_null => 1 ), },
+	   ],
+   index  => { FromIndex => [ 'FromDate' ], 
+               ToIndex   => [ 'ToDate' ], 
+               DocIndex  => [ 'Document_ID' ], 
+             },
+   fk     => { Document => { Document_ID => 'ID' } },
+  );
+
 1;
 
 __END__
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/MODULES.TXT mkd/MODULES.TXT
--- mkd-1.6/MODULES.TXT	2004-09-29 12:08:29.000000000 -0400
+++ mkd/MODULES.TXT	2004-10-19 12:18:31.000000000 -0400
@@ -35,6 +35,8 @@
 # Time & Date manipulation:
 	Date::Manip		v5.52
 	Time::ParseDate
+        DateTime
+        DateTime::TimeZone
 
 # Image manipulation:
 	Image::Magick		v5.48
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/t/timerange.t mkd/t/timerange.t
--- mkd-1.6/t/timerange.t	1969-12-31 19:00:00.000000000 -0500
+++ mkd/t/timerange.t	2004-10-23 14:00:42.000000000 -0400
@@ -0,0 +1,189 @@
+use strict;
+use warnings;
+use Test::More qw(no_plan);
+use MKDoc;
+use DateTime;
+use DateTime::TimeZone;
+
+# make sure SITE_DIR is set since MKDoc->init needs it
+die "SITE_DIR isn't set.  Please source mksetenv.sh from an installed MKDoc site ".
+  "and try again.\n" 
+  unless $ENV{SITE_DIR};
+
+# initialize MKDoc, needed for database connection
+MKDoc->init;
+
+use_ok('flo::editor::TimeRange');
+
+my $tr = flo::editor::TimeRange->new();
+isa_ok($tr, 'flo::editor::TimeRange');
+
+# create from now to an hour from now
+my $now = time;
+my $later = $now + 60 * 60;
+my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($now);
+
+# get current timezone from DateTime
+my $tz = DateTime->now(time_zone => "local")->time_zone->name;
+ok($tz);
+isa_ok(DateTime::TimeZone->new(name => $tz), 'DateTime::TimeZone');
+
+# setup dates and make sure accessors are working
+$tr->set_from_year($year + 1900);
+is($tr->from_year(), $year + 1900);
+$tr->set_from_month($mon + 1);
+is($tr->from_month(), $mon + 1);
+$tr->set_from_day($mday);
+is($tr->from_day(), $mday);
+$tr->set_from_hour($hour);
+is($tr->from_hour(), $hour);
+$tr->set_from_minute($min);
+is($tr->from_minute(), $min);
+$tr->set_from_tz($tz);
+is($tr->from_tz(), $tz);
+
+# check that from_datetime is working
+my $dt = $tr->from_datetime;
+isa_ok($dt, 'DateTime');
+foreach my $field (qw(year month day hour minute)) {
+    my $meth = "from_$field";
+    is($dt->$field, $tr->$meth, "Check that $field matches in from_datetime");
+}
+
+($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($later);
+
+$tr->set_to_year($year + 1900);
+is($tr->to_year(), $year + 1900);
+$tr->set_to_month($mon + 1);
+is($tr->to_month(), $mon + 1);
+$tr->set_to_day($mday);
+is($tr->to_day(), $mday);
+$tr->set_to_hour($hour);
+is($tr->to_hour(), $hour);
+$tr->set_to_minute($min);
+is($tr->to_minute(), $min);
+$tr->set_to_tz($tz);
+is($tr->to_tz(), $tz);
+
+# check that to_datetime is working
+$dt = $tr->to_datetime;
+isa_ok($dt, 'DateTime');
+foreach my $field (qw(year month day hour minute)) {
+    my $meth = "to_$field";
+    is($dt->$field, $tr->$meth, "Check that $field matches in to_datetime");
+}
+
+# test the month selector
+my $select = $tr->month_select("from");
+is(@$select, 12);
+my @selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_month);
+
+# test the day selector
+$select = $tr->day_select("from");
+is(@$select, 31);
+ at selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_day);
+
+# test the year selector
+$select = $tr->year_select("from");
+is($select->[0]{value}, (localtime)[5] + 1900);
+ at selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_year);
+
+# test the hour selector
+$select = $tr->hour_select("from");
+is(@$select, 24);
+ at selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_hour);
+
+# test the minute selector
+$select = $tr->minute_select("from");
+is(@$select, 60);
+ at selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_minute);
+
+# test the tz selector
+$select = $tr->tz_select("from");
+ok(@$select > 10);
+ at selected = grep { $_->{is_selected} } @$select;
+is(@selected, 1);
+is($selected[0]->{value}, $tr->from_tz);
+
+# test validation
+my $bad = flo::editor::TimeRange->new();
+isa_ok($tr, 'flo::editor::TimeRange');
+
+# empty title is an error
+has_error($bad, 'component/timerange/title_empty');
+
+# test equality detection, both should default to now
+$bad->set_title('foo');
+has_error($bad, 'component/timerange/dates_equal');
+
+# test reversal detection
+$bad->set_from_hour($bad->from_hour + 1);
+has_error($bad, 'component/timerange/dates_reversed');
+
+# now it should be ok
+$bad->set_to_hour($bad->to_hour + 2);
+$bad->validate;
+ok(not $bad->has_errors);
+
+# check that a nonsense date fails
+$bad->set_from_month(2);
+$bad->set_from_day(31);
+has_error($bad, "component/timerange/from_invalid");
+
+
+# set it back good
+$bad->set_from_month($mon + 1);
+$bad->set_from_day($mday);
+
+# try to write to the index
+ok($bad->write_index());
+ok($bad->Document_TimeRange_ID);
+
+# check that data is correct
+my $dbh = lib::sql::DBH->get();
+my ($db_from, $db_to) = $dbh->selectrow_array('SELECT FromDate, ToDate 
+                                               FROM Document_TimeRange
+                                               WHERE ID = ?', undef, 
+                                              $bad->Document_TimeRange_ID);
+ok($db_from =~ m/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/);
+my ($db_year, $db_month, $db_day, $db_hour, $db_minute, $db_second) =
+  ($1, $2, $3, $4, $5, $6);
+
+# the date should match the current date in UTC
+my $utc = $bad->from_datetime;
+$utc->set_time_zone("UTC");
+cmp_ok($db_year,  '==', $utc->year);
+cmp_ok($db_month, '==', $utc->month);
+cmp_ok($db_day,   '==', $utc->day);
+cmp_ok($db_hour,  '==', $utc->hour);
+cmp_ok($db_second,'==', $utc->second);
+
+# cleanup test data
+$dbh->do('DELETE FROM Document_TimeRange WHERE ID = ?', 
+         undef, $bad->Document_TimeRange_ID);
+
+#
+# Utility Routines
+#
+
+# has_error($obj, $key) - calls $obj->validate(), checks that the
+# object has an error and that one of the errors matches the specified
+# key.  Clears the list of errors before returning.
+sub has_error {
+    my ($obj, $key) = @_;
+    $bad->validate();
+    ok($obj->has_errors, "Checking for errors at line " . (caller)[2]);
+    ok((grep { $_->is($key) } @{$obj->errors}), 
+       "Looking for the $key error at line " . (caller)[2]);
+    $obj->{'.errors'} = [];   
+}
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/templates/component/headlines/en.html mkd/templates/component/headlines/en.html
--- mkd-1.6/templates/component/headlines/en.html	2004-11-03 15:34:28.658770072 -0500
+++ mkd/templates/component/headlines/en.html	2004-10-24 15:27:05.000000000 -0400
@@ -76,43 +76,88 @@
         >your preferences</a>:
       </p>
       <dl>
-        <div
-          petal:repeat="headline self/personalized_headlines" 
+        <!--? Generate upcoming events style headlines -->
+        <div 
+          petal:condition="true: self/mode_is_upcoming"
+          petal:repeat="row self/personalized_headlines" 
           petal:omit-tag="string:1"
         >
-          <dt>
+          <dt petal:define="headline row/document; timerange row/timerange;">
             <a
-              href="#"
-              hreflang="en"
+                href="#"
+                hreflang="en"
+                lang="en"
+                xml:lang="en"
+                dir="ltr"
+                petal:attributes="href headline/uri; 
+                                  hreflang headline/lang; 
+                                  lang headline/lang; 
+                                  xml:lang headline/lang; 
+                                  dir headline/direction;"
+                petal:content="headline/title"
+              >Title of the document</a>
+          </dt>
+          <dd> 
+            <span
               lang="en"
               xml:lang="en"
               dir="ltr"
-              petal:attributes="
-                                href headline/uri; 
-                                hreflang headline/lang; 
-                                lang headline/lang; 
+              petal:attributes="lang headline/lang; 
                                 xml:lang headline/lang; 
-                                dir headline/direction;
-                               "
-              petal:content="headline/title"
-            >Pumpkins on fire!</a>
+                                dir headline/direction;"
+              petal:content="timerange/title"
+            >
+              A description of the event.
+            </span>
+            <br />
+            From: 
+            <small petal:content="timerange/from_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+             DATE
+            </small>
+            To: 
+            <small petal:content="timerange/to_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+             DATE
+            </small>
+          </dd>
+        </div>
+
+
+        <!--? Generate newest documents style headlines -->
+        <div 
+          petal:condition="true: self/mode_is_newest"
+          petal:repeat="headline self/personalized_headlines" 
+          petal:omit-tag="string:1"
+        >
+          <dt>
+            <a
+                href="#"
+                hreflang="en"
+                lang="en"
+                xml:lang="en"
+                dir="ltr"
+                petal:attributes="href headline/uri; 
+                                  hreflang headline/lang; 
+                                  lang headline/lang; 
+                                  xml:lang headline/lang; 
+                                  dir headline/direction;"
+                petal:content="headline/title"
+              >Title of the document</a>
           </dt>
           <dd> 
             <span
               lang="en"
               xml:lang="en"
               dir="ltr"
-              petal:attributes="
-                                lang headline/lang; 
+              petal:attributes="lang headline/lang; 
                                 xml:lang headline/lang; 
                                 dir headline/direction;"
               petal:content="headline/description"
             >
-              Some description describing some stuff about fire and pumpkins.
+              A description of the document.
             </span>
             <br />
             <small petal:content="headline/date_created"> 
-             DATE 
+               DATE
             </small>
           </dd>
         </div>
@@ -140,41 +185,88 @@
         or because none of the documents in the list matched them.
       </p>
       <dl>
+        <!--? Generate upcoming events style headlines -->
         <div 
-          petal:repeat="headline self/default_headlines" 
+          petal:condition="true: self/mode_is_upcoming"
+          petal:repeat="row self/default_headlines" 
           petal:omit-tag="string:1"
         >
-          <dt>
+          <dt petal:define="headline row/document; timerange row/timerange;">
             <a
-              href="#"
-              hreflang="en"
+                href="#"
+                hreflang="en"
+                lang="en"
+                xml:lang="en"
+                dir="ltr"
+                petal:attributes="href headline/uri; 
+                                  hreflang headline/lang; 
+                                  lang headline/lang; 
+                                  xml:lang headline/lang; 
+                                  dir headline/direction;"
+                petal:content="headline/title"
+              >Title of the document</a>
+          </dt>
+          <dd> 
+            <span
               lang="en"
               xml:lang="en"
               dir="ltr"
-              petal:attributes="href headline/uri; 
-                                hreflang headline/lang; 
-                                lang headline/lang; 
+              petal:attributes="lang headline/lang; 
                                 xml:lang headline/lang; 
                                 dir headline/direction;"
-              petal:content="headline/title"
-            >Pumpkins on fire!</a>
+              petal:content="timerange/title"
+            >
+              A description of the event.
+            </span>
+            <br />
+            From: 
+            <small petal:content="timerange/from_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+             DATE
+            </small>
+            To: 
+            <small petal:content="timerange/to_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+             DATE
+            </small>
+          </dd>
+        </div>
+
+
+        <!--? Generate newest documents style headlines -->
+        <div 
+          petal:condition="true: self/mode_is_newest"
+          petal:repeat="headline self/default_headlines" 
+          petal:omit-tag="string:1"
+        >
+          <dt>
+            <a
+                href="#"
+                hreflang="en"
+                lang="en"
+                xml:lang="en"
+                dir="ltr"
+                petal:attributes="href headline/uri; 
+                                  hreflang headline/lang; 
+                                  lang headline/lang; 
+                                  xml:lang headline/lang; 
+                                  dir headline/direction;"
+                petal:content="headline/title"
+              >Title of the document</a>
           </dt>
-          <dd>
+          <dd> 
             <span
               lang="en"
               xml:lang="en"
               dir="ltr"
-              petal:attributes="
-                                lang headline/lang; 
+              petal:attributes="lang headline/lang; 
                                 xml:lang headline/lang; 
                                 dir headline/direction;"
               petal:content="headline/description"
             >
-              Some description describing some stuff about fire and pumpkins.
+              A description of the document.
             </span>
             <br />
             <small petal:content="headline/date_created"> 
-             DATE
+               DATE
             </small>
           </dd>
         </div>
@@ -187,7 +279,56 @@
     petal:condition="false: self/is_user_logged_in"
   >
     <dl>
+
+      <!--? Generate upcoming events style headlines -->
+      <div 
+        petal:condition="true: self/mode_is_upcoming"
+        petal:repeat="row self/default_headlines" 
+        petal:omit-tag="string:1"
+      >
+        <dt petal:define="headline row/document; timerange row/timerange;">
+          <a
+              href="#"
+              hreflang="en"
+              lang="en"
+              xml:lang="en"
+              dir="ltr"
+              petal:attributes="href headline/uri; 
+                                hreflang headline/lang; 
+                                lang headline/lang; 
+                                xml:lang headline/lang; 
+                                dir headline/direction;"
+              petal:content="headline/title"
+            >Title of the document</a>
+        </dt>
+        <dd> 
+          <span
+            lang="en"
+            xml:lang="en"
+            dir="ltr"
+            petal:attributes="lang headline/lang; 
+                              xml:lang headline/lang; 
+                              dir headline/direction;"
+            petal:content="timerange/title"
+          >
+            A description of the event.
+          </span>
+          <br />
+          From: 
+          <small petal:content="timerange/from_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+           DATE
+          </small>
+          To: 
+          <small petal:content="timerange/to_datetime/strftime '%B %e, %Y at %H:%M %Z'"> 
+           DATE
+          </small>
+        </dd>
+      </div>
+
+
+      <!--? Generate newest documents style headlines -->
       <div 
+        petal:condition="true: self/mode_is_newest"
         petal:repeat="headline self/default_headlines" 
         petal:omit-tag="string:1"
       >
@@ -198,36 +339,34 @@
               lang="en"
               xml:lang="en"
               dir="ltr"
-              petal:attributes="
-                                href headline/uri; 
+              petal:attributes="href headline/uri; 
                                 hreflang headline/lang; 
                                 lang headline/lang; 
                                 xml:lang headline/lang; 
-                                dir headline/direction;
-                               "
+                                dir headline/direction;"
               petal:content="headline/title"
-            >Pumpkins on fire!</a>
+            >Title of the document</a>
         </dt>
         <dd> 
           <span
             lang="en"
             xml:lang="en"
             dir="ltr"
-            petal:attributes="
-                              lang headline/lang; 
+            petal:attributes="lang headline/lang; 
                               xml:lang headline/lang; 
-                              dir headline/direction;
-                             "
+                              dir headline/direction;"
             petal:content="headline/description"
           >
-            Some description describing some stuff about fire and pumpkins.
+            A description of the document.
           </span>
-            <br />
-            <small petal:content="headline/date_created"> 
+          <br />
+          <small petal:content="headline/date_created"> 
              DATE
-            </small>
+          </small>
         </dd>
       </div>
+
     </dl>
+
   </div>
 </div>
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/templates/editor/headlines/en.html mkd/templates/editor/headlines/en.html
--- mkd-1.6/templates/editor/headlines/en.html	2004-03-18 10:48:40.000000000 -0500
+++ mkd/templates/editor/headlines/en.html	2004-10-23 19:10:38.000000000 -0400
@@ -15,6 +15,7 @@
                 name_title         string:${self/block_name}_title;
                 name_from_path     string:${self/block_name}_from_path;
                 name_leaf_only     string:${self/block_name}_leaf_only;
+                name_mode          string:${self/block_name}_mode;
                 align              self/align;
                 align_opposite     self/align_opposite;
                 dir                self/direction"
@@ -59,6 +60,10 @@
       petal:condition="error/is --component/headlines/max_headlines_malformed"
     >You must enter a natural number for the 'Number of headlines' field.
      No characters other than 0 to 9 are allowed.</p>
+    <p
+      xml:lang="en" lang="en" dir="ltr" class="error"
+      petal:condition="error/is --component/headlines/mode_invalid"
+    >You must choose a mode.</p>
   </div>
 
     <p
@@ -179,6 +184,52 @@
         xml:lang="en"
         lang="en"
         dir="ltr"
+      >The mode which determines headline selection.  Choose 'Newest Documents' to show the latest documents created.  Choose 'Upcoming Events' to show those with time-ranges occuring soonest.</em>
+      <label
+        for="headlines_mode"
+        xml:lang="en"
+        lang="en"
+        dir="ltr"
+        petal:attributes="for name_mode"
+      >Mode</label>
+      <br />
+      <select
+        name="headlines_mode"
+        id="headlines_mode"
+        title="Choose the headline mode here."
+        petal:attributes="name name_mode; id name_mode"
+      >
+          <option 
+            selected="selected"
+            value="newest"
+            title="Newest Documents"
+            petal:condition="true: self/mode_is_newest"
+          >Newest Documents</option>
+          <option 
+            value="newest"
+            title="Newest Documents"
+            petal:condition="false: self/mode_is_newest"
+          >Newest Documents</option>
+          <option 
+            selected="selected"
+            value="upcoming"
+            title="Upcoming Events"
+            petal:condition="true: self/mode_is_upcoming"
+          >Upcoming Events</option>
+          <option 
+            value="upcoming"
+            title="Upcoming Events"
+            petal:condition="false: self/mode_is_upcoming"
+          >Upcoming Events</option>
+     </select>
+    </p>
+
+    <p>
+      <em
+        class="help"
+        xml:lang="en"
+        lang="en"
+        dir="ltr"
       >
         The maximum number of documents to be listed.
       </em>
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/templates/editor/timerange/en.html mkd/templates/editor/timerange/en.html
--- mkd-1.6/templates/editor/timerange/en.html	2004-11-03 15:34:29.677615184 -0500
+++ mkd/templates/editor/timerange/en.html	2004-10-24 18:23:58.000000000 -0400
@@ -203,62 +203,50 @@
       >
           <option 
             value="1"
-            title="1"
             petal:attributes="selected from_months/0/is_selected"
           >January</option>
           <option 
             value="2"
-            title="2"
             petal:attributes="selected from_months/1/is_selected"
           >February</option>
           <option 
             value="3"
-            title="3"
             petal:attributes="selected from_months/2/is_selected"
           >March</option>
           <option 
             value="4"
-            title="4"
             petal:attributes="selected from_months/3/is_selected"
           >April</option>
           <option 
             value="5"
-            title="5"
             petal:attributes="selected from_months/4/is_selected"
           >May</option>
           <option 
             value="6"
-            title="6"
             petal:attributes="selected from_months/5/is_selected"
           >June</option>
           <option 
             value="7"
-            title="7"
             petal:attributes="selected from_months/6/is_selected"
           >July</option>
           <option 
             value="8"
-            title="8"
             petal:attributes="selected from_months/7/is_selected"
           >August</option>
           <option 
             value="9"
-            title="9"
             petal:attributes="selected from_months/8/is_selected"
           >September</option>
           <option 
             value="10"
-            title="10"
             petal:attributes="selected from_months/9/is_selected"
           >October</option>
           <option 
             value="11"
-            title="11"
             petal:attributes="selected from_months/10/is_selected"
           >November</option>
           <option 
             value="12"
-            title="12"
             petal:attributes="selected from_months/11/is_selected"
           >December</option>
       </select>
@@ -280,12 +268,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    day/value;"
+            petal:attributes="value    day/value;
+                              title    day/value"
             petal:content="day/label"
             petal:condition="true: day/is_selected"
           >01</option>
           <option 
-            petal:attributes="value    day/value;"
+            petal:attributes="value    day/value;
+                              title    day/value"
             petal:content="day/label"
             petal:condition="false: day/is_selected"
           >02</option>
@@ -309,12 +299,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    year/value;"
+            petal:attributes="value    year/value;
+                              title    year/value"
             petal:content="year/label"
             petal:condition="true: year/is_selected"
           >2004</option>
           <option 
-            petal:attributes="value    year/value;"
+            petal:attributes="value    year/value;
+                              title    year/value"
             petal:content="year/label"
             petal:condition="false: year/is_selected"
           >2004</option>
@@ -338,12 +330,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    hour/value;"
+            petal:attributes="value    hour/value;
+                              title    hour/value"
             petal:content="hour/label"
             petal:condition="true: hour/is_selected"
           >00</option>
           <option 
-            petal:attributes="value    hour/value;"
+            petal:attributes="value    hour/value;
+                              title    hour/value"
             petal:content="hour/label"
             petal:condition="false: hour/is_selected"
           >01</option>
@@ -367,12 +361,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    minute/value;"
+            petal:attributes="value    minute/value;
+                              title    minute/value"
             petal:content="minute/label"
             petal:condition="true: minute/is_selected"
           >00</option>
           <option 
-            petal:attributes="value    minute/value;"
+            petal:attributes="value    minute/value;
+                              title    minute/value"
             petal:content="minute/label"
             petal:condition="false: minute/is_selected"
           >00</option>
@@ -443,62 +439,50 @@
       >
           <option 
             value="1"
-            title="1"
             petal:attributes="selected to_months/0/is_selected"
           >January</option>
           <option 
             value="2"
-            title="2"
             petal:attributes="selected to_months/1/is_selected"
           >February</option>
           <option 
             value="3"
-            title="3"
             petal:attributes="selected to_months/2/is_selected"
           >March</option>
           <option 
             value="4"
-            title="4"
             petal:attributes="selected to_months/3/is_selected"
           >April</option>
           <option 
             value="5"
-            title="5"
             petal:attributes="selected to_months/4/is_selected"
           >May</option>
           <option 
             value="6"
-            title="6"
             petal:attributes="selected to_months/5/is_selected"
           >June</option>
           <option 
             value="7"
-            title="7"
             petal:attributes="selected to_months/6/is_selected"
           >July</option>
           <option 
             value="8"
-            title="8"
             petal:attributes="selected to_months/7/is_selected"
           >August</option>
           <option 
             value="9"
-            title="9"
             petal:attributes="selected to_months/8/is_selected"
           >September</option>
           <option 
             value="10"
-            title="10"
             petal:attributes="selected to_months/9/is_selected"
           >October</option>
           <option 
             value="11"
-            title="11"
             petal:attributes="selected to_months/10/is_selected"
           >November</option>
           <option 
             value="12"
-            title="12"
             petal:attributes="selected to_months/11/is_selected"
           >December</option>
       </select>
@@ -519,12 +503,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    day/value;"
+            petal:attributes="value    day/value;
+                              title    day/label;"
             petal:content="day/label"
             petal:condition="true: day/is_selected"
           >January</option>
           <option 
-            petal:attributes="value    day/value;"
+            petal:attributes="value    day/value;
+                              title    day/value;"
             petal:content="day/label"
             petal:condition="false: day/is_selected"
           >Feburary</option>
@@ -548,12 +534,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    year/value;"
+            petal:attributes="value    year/value;
+                              title    year/value"
             petal:content="year/label"
             petal:condition="true: year/is_selected"
           >2004</option>
           <option 
-            petal:attributes="value    year/value;"
+            petal:attributes="value    year/value;
+                              title    year/value"
             petal:content="year/label"
             petal:condition="false: year/is_selected"
           >2005</option>
@@ -577,12 +565,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    hour/value;"
+            petal:attributes="value    hour/value;
+                              title    hour/value;"
             petal:content="hour/label"
             petal:condition="true: hour/is_selected"
           >00</option>
           <option 
-            petal:attributes="value    hour/value;"
+            petal:attributes="value    hour/value;
+                              title    hour/value;"
             petal:content="hour/label"
             petal:condition="false: hour/is_selected"
           >01</option>
@@ -606,12 +596,14 @@
         >
           <option 
             selected="selected"
-            petal:attributes="value    minute/value;"
+            petal:attributes="value    minute/value;
+                              title    minute/value"
             petal:content="minute/label"
             petal:condition="true: minute/is_selected"
           >00</option>
           <option 
-            petal:attributes="value    minute/value;"
+            petal:attributes="value    minute/value;
+                              title    minute/value"
             petal:content="minute/label"
             petal:condition="false: minute/is_selected"
           >01</option>
@@ -641,7 +633,7 @@
           >BST</option>
           <option 
             petal:attributes="value    tz/value;
-                              title    tz/value;"
+                              title    tz/label;"
             petal:content="tz/label"
             petal:condition="false: tz/is_selected"
           >GMT</option>
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/templates/fragments/menu/en.html mkd/templates/fragments/menu/en.html
--- mkd-1.6/templates/fragments/menu/en.html	2004-11-03 15:34:31.921274096 -0500
+++ mkd/templates/fragments/menu/en.html	2004-03-18 10:48:42.000000000 -0500
@@ -38,37 +38,6 @@
       petal:attributes="dir dir"
     >|</bdo>
 
-    <!--? The following loop generates a list of the non-hidden
-          child documents of the root document ?-->
-    <div
-      petal:omit-tag=""
-      petal:repeat="child self/root/children_showable"
-      petal:condition="true: self/root/children_showable"
-    >
-      <a
-        href="http://example.com/"
-        hreflang="en"
-        xml:lang="en"
-        lang="en"
-        dir="ltr"
-        petal:content="child/title"
-        petal:attributes="href     child/uri; 
-                          hreflang child/lang; 
-                          dir      child/direction; 
-                          lang     child/lang; 
-                          xml:lang child/lang;
-                          title    child/description;"
-        petal:omit-tag="true: self/equals child "
-      ><strong 
-         petal:omit-tag="false: self/equals child" 
-         petal:content="child/title"
-       >Document Title</strong></a>
-      <bdo 
-        dir="ltr"
-        petal:attributes="dir dir"
-      >|</bdo>
-    </div>
-
 <!--?
 
   The following can be used to create menu items that don't link to themselves.
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/templates/newsletter/en.xml mkd/templates/newsletter/en.xml
--- mkd-1.6/templates/newsletter/en.xml	2003-09-12 11:02:53.000000000 -0400
+++ mkd/templates/newsletter/en.xml	2004-11-02 23:50:59.000000000 -0500
@@ -46,6 +46,8 @@
     matches your preferences.
   </p>
 
+<span petal:condition="self/has_newest">
+
 <p petal:condition="self/is_daily">
   The following documents added yesterday may be of interest to you:
 </p>
@@ -56,19 +58,41 @@
   The following documents added in the last month may be of interest to you:
 </p>
 
-<pre petal:repeat="result self/results">${result/Title}
+<pre petal:repeat="result self/results --newest">${result/Title}
     &lt;${result/Full_Path}&gt;
 ${result/Description}
     (Created by ${result/Creator_First_Name} ${result/Creator_Family_Name} on ${result/Date_Created})</pre>
 
-  <p>
+</span>
+
+<span petal:condition="self/has_upcoming">
+
+<p petal:condition="self/is_daily">
+  The following events occuring today may be of interest to you:
+</p>
+<p petal:condition="self/is_weekly">
+  The following events occuring this week may be of interest to you:
+</p>
+<p petal:condition="self/is_monthly">
+  The following events occuring this month may be of interest to you:
+</p>
+
+<pre petal:repeat="result self/results --upcoming">${result/Title}
+    &lt;${result/Full_Path}&gt;
+${result/TimeRange_Title}
+    (${result/TimeRange_FromDate/strftime '%B %e, %Y at %H:%M %Z'} - ${result/TimeRange_ToDate/strftime '%B %e, %Y at %H:%M %Z'})</pre>
+
+
+    <p>
     If you no longer want to<br /> receive these email updates or want to
     change your preferences you will need to edit your account at:
     </p>
 
   <pre><span petal:replace="string: &lt;${details/user_domain_notrailingslash}/${preferences/name}&gt;">&lt;http://example.com/preferences.mumble&gt;</span></pre>
 
-  <p>                                                                                                              
+</span>
+
+  <p>                                                                                   
     If you can't remember your account password, you can get an email
     reminder by visiting this link:
   </p>
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/timerange_update_db.pl mkd/timerange_update_db.pl
--- mkd-1.6/timerange_update_db.pl	1969-12-31 19:00:00.000000000 -0500
+++ mkd/timerange_update_db.pl	2004-10-23 14:34:15.000000000 -0400
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use MKDoc;
+
+# make sure SITE_DIR is set since MKDoc->init needs it
+die "SITE_DIR isn't set.  Please source mksetenv.sh from an installed MKDoc site ".
+  "and try again.\n" 
+  unless $ENV{SITE_DIR};
+
+# initialize MKDoc, needed for database connection
+MKDoc->init;
+
+print "Creating the Document_TimeRange table...\n";
+my $dbh = lib::sql::DBH->get();
+$dbh->do(<<END);
+    CREATE TABLE Document_TimeRange (
+      ID int(11)          NOT NULL auto_increment,
+      Document_ID int(11) NOT NULL default '0',
+      FromDate datetime   NOT NULL default '0000-00-00 00:00:00',
+      ToDate datetime     NOT NULL default '0000-00-00 00:00:00',
+      PRIMARY KEY   (ID),
+      KEY DocIndex  (Document_ID),
+      KEY FromIndex (FromDate),
+      KEY ToIndex   (ToDate)
+    ) TYPE=MyISAM
+END
diff --exclude=CVS --exclude=conf --exclude=mksetenv.sh --exclude=ECommerce --exclude=shop -Naur mkd-1.6/tools/cron/020..newsletter.pl mkd/tools/cron/020..newsletter.pl
--- mkd-1.6/tools/cron/020..newsletter.pl	2004-03-29 10:24:45.000000000 -0500
+++ mkd/tools/cron/020..newsletter.pl	2004-11-03 15:30:50.763895128 -0500
@@ -8,6 +8,7 @@
 use Encode;
 use base qw /flo::Plugin/;
 use Petal::Mail;
+use Carp qw(croak);
 
 sub template_path { '/newsletter' }
 
@@ -43,16 +44,73 @@
     return 1; 
 }
 
+# returns true if there are newest documents to include in the newsletter
+sub has_newest {
+    my $self = shift;
+    my $res = $self->results("newest");
+    return 1 if @$res;
+    return 0;
+}
 
-sub results
-{
+# returns true if there are upcoming events to include in the newsletter
+sub has_upcoming {
+    my $self = shift;
+    my $res = $self->results("upcoming");
+    return 1 if @$res;
+    return 0;
+}
+
+# return ref to array of hashes containing newsletter results.  Takes
+# a single parameter specifying the mode, either "newest" or
+# "upcoming".  This method cache the results for each mode.
+sub results {
     my $self = shift;    
+    my $mode = shift || croak("Missing required mode parameter");
+
+    # return cached data if available
+    return $self->{"_cache_$mode"} if $self->{"_cache_$mode"};
+
     my $user_login = $self->user()->login();
-    my $date_since = $self->compute_date_since();
-    
+
+    # concoct SQL needed for upcoming or newest mode
+    my ($extra_from, $extra_where, $extra_select, @extra_params,
+        $order_by, $group_by);
+        
+    if ($mode eq 'upcoming') {
+        # setup for selecting by timerange
+        $order_by     = "Document_TimeRange.FromDate ASC";
+        $group_by     = "Document_TimeRange.ID";
+        $extra_from   = ", Document_TimeRange";
+        $extra_select = ", Document_TimeRange.ID as Document_TimeRange_ID";
+
+        # select documents with timeranges dates from now to the end
+        # of the range
+        $extra_where  = <<END;
+          AND Document.ID = Document_TimeRange.Document_ID
+          AND ((Document_TimeRange.FromDate >= ? AND 
+                Document_TimeRange.FromDate <= ?)
+               OR 
+               (Document_TimeRange.ToDate >= ? AND 
+                Document_TimeRange.ToDate <= ?))
+END
+        @extra_params = ($self->compute_date_now(), 
+                         $self->compute_date_forward()) x 2;
+    } else {
+        # select documents created in the chosen period
+        $order_by     = "Date_Created DESC";
+        $group_by     = "Document.ID";
+        $extra_where  = "AND Document.Date_Created > ?";
+        ($extra_from, $extra_select) = ("") x 2;
+        @extra_params = $self->compute_date_since();
+    }
+
     my $sql  = <<SQL;
-SELECT Document.ID AS ID, SUM(Preference_Audience.Value) AS Pref_Score
+SELECT Document.ID AS ID, SUM(Preference_Audience.Value) AS Pref_Score,
+       Creator.First_Name as Creator_First_Name,
+       Creator.Family_Name as Creator_Family_Name
+       $extra_select
 FROM   Document, Document_Audience, Audience, Preference_Audience, Editor, Editor as Creator, Preference_Language
+       $extra_from
 WHERE
         -- join the tables together
         (
@@ -70,68 +128,92 @@
 AND
         -- remove languages which are not wanted
         Preference_Language.Value = 1
-AND
-        -- get only documents which are recent enough
-       Document.Date_Created > ?
+$extra_where
+
 
-GROUP BY (Document.ID)
-ORDER BY Document.Full_Path
+GROUP BY $group_by
+ORDER BY $order_by
 SQL
 
     my $dbh = lib::sql::DBH->get();
     my $sth = $dbh->prepare_cached ($sql);
-    $sth->execute ($user_login, $date_since);
+    $sth->execute ($user_login, @extra_params);
     
-    my @res = ();
+    my @res;
     my $doc_t = flo::Standard::table ('Document');
-    while (my $h = $sth->fetchrow_hashref)
-    {
+    ROW: while (my $h = $sth->fetchrow_hashref) {
         next unless ($h->{Pref_Score} > 0);
 	my $doc = $doc_t->get ( $h->{ID} );
 	next unless ($doc->is_showable());
 	
 	$Text::Wrap::columns = 72;
 	$Text::Wrap::columns = 72;
-	$h->{Full_Path} =~ s/^\//$::PUBLIC_DOMAIN/;
-	$h->{Date_Created} =~ s/\s+.*//;
-	$h->{Description} = join '', fill ('    ', '    ', $h->{Description});
-	push @res, $h;
+
+        # assemble data for template from the document object and the
+        # query results
+        my %row;
+        $row{doc}           = $doc;
+        ($row{Full_Path}    = $doc->{Full_Path}) =~ s/^\//$::PUBLIC_DOMAIN/;
+        ($row{Date_Created} = $doc->date_created) =~ s/\s+.*//;
+        $row{Description}   = join '',fill ('    ', '    ', $doc->description);
+        $row{Title}         = $doc->title;
+        $row{Creator_First_Name}  = $h->{Creator_First_Name};
+        $row{Creator_Family_Name} = $h->{Creator_Family_Name};
+
+        # deal with timerange data for upcoming events list
+        if ($mode eq 'upcoming') {
+            # find this timerange in the list of components for this doc
+            my $timerange_id = $h->{Document_TimeRange_ID};
+            my ($timerange) = 
+              grep { $_->isa('flo::editor::TimeRange') and
+                     $_->Document_TimeRange_ID == $timerange_id }
+                $doc->components;
+            # ignore hits with no matching timerange.
+            next ROW unless $timerange;
+
+            $row{TimeRange_Title}    = $timerange->title;
+            $row{TimeRange_FromDate} = $timerange->from_datetime;
+            $row{TimeRange_ToDate}   = $timerange->to_datetime;
+        }
+
+	push @res, \%row;
     }
     
-    return \@res;
+    # save for future requests
+    return $self->{"_cache_$mode"} = \@res;
+}
+
+# translate a time to day-month-year for use with MySQL
+sub _time2date {
+    my $self = shift;
+    my ($mday, $mon, $year) = (localtime(shift))[3,4,5];
+    $mon += 1;
+    $year += 1900;
+    return "$year-$mon-$mday";
 }
 
+sub compute_date_now { shift->_time2date(time()) }
 
 package MKDoc::Misc::Newsletter::Daily;
 use strict;
 use warnings;
 our @ISA = qw /MKDoc::Misc::Newsletter/;
 
-sub compute_date_since
-{
-    my $self = shift;
-    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time - 24 * 3600);
-    $mon += 1;
-    $year += 1900;
-    return "$year-$mon-$mday";
-}
+sub compute_date_since { shift->_time2date(time - 24 * 3600) }
 
-sub is_daily { 1 }
+sub compute_date_forward { shift->_time2date(time + 24 * 3600) . " 23:59:59" }
 
+sub is_daily { 1 }
 
 package MKDoc::Misc::Newsletter::Weekly;
 use strict;
 use warnings;
 our @ISA = qw /MKDoc::Misc::Newsletter/;
 
-sub compute_date_since
-{
-    my $self = shift;
-    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time - 7 * 24 * 3600);
-    $mon += 1;
-    $year += 1900;
-    return "$year-$mon-$mday";
-}
+sub compute_date_since { shift->_time2date(time - 7 * 24 * 3600) }
+
+sub compute_date_forward { shift->_time2date(time + 7 * 24 * 3600) . 
+                             " 23:59:59" }
 
 sub is_weekly { 1 }
 
@@ -141,10 +223,9 @@
 use warnings;
 our @ISA = qw /MKDoc::Misc::Newsletter/;
 
-sub compute_date_since
-{
+sub compute_date_since {
     my $self = shift;
-    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime (time);
+    my ($mday, $mon, $year) = (localtime(time))[3,4,5];
     $year += 1900;
     ($mon == 0) and do {
 	$year--;
@@ -154,6 +235,19 @@
     return "$year-$mon-$mday";
 }
 
+sub compute_date_forward {
+    my $self = shift;
+    my ($mday, $mon, $year) = (localtime(time))[3,4,5];
+    $year += 1900;
+    $mon  += 2;
+    ($mon == 13) and do {
+	$year++;
+	$mon = 1;
+    };
+    
+    return "$year-$mon-$mday 23:59:59";
+}
+
 sub is_monthly { 1 }
 
 
@@ -206,8 +300,8 @@
     ($periodicity eq WEEKLY)  and $news = new MKDoc::Misc::Newsletter::Weekly (%args);
     ($periodicity eq MONTHLY) and $news = new MKDoc::Misc::Newsletter::Monthly (%args);
     
-    my $res = $news->results();
-    @{$res} or return;
+    # don't send a newsletter if there's no news
+    return unless $news->has_upcoming or $news->has_newest;
     return $news;
 }
 


More information about the MKDoc-dev mailing list