[MKDoc-commit] Add events component (Sam Tregar).
bruno at mkdoc.demon.co.uk
bruno at mkdoc.demon.co.uk
Wed Nov 10 17:42:18 GMT 2004
Log Message:
-----------
Add events component (Sam Tregar). Fix broken newsletter (Sam Tregar)
Tags:
----
mkdoc-1-6
Modified Files:
--------------
mkd:
INSTALL.TXT
MODULES.TXT
mkd/MKDoc/Site/Deploy/DB:
Schema.pm
mkd/flo/Record:
Document.pm
mkd/flo/editor:
Headlines.pm
mkd/flo/plugin/Admin:
Content.pm
mkd/lib/sql:
DBH.pm
mkd/templates/component/headlines:
en.html
mkd/templates/editor/headlines:
en.html
mkd/templates/newsletter:
en.xml
mkd/tools/cron:
020..newsletter.pl
Added Files:
-----------
mkd:
timerange_update_db.pl
mkd/flo/editor:
TimeRange.pm
mkd/t:
timerange.t
-------------- next part --------------
Index: Content.pm
===================================================================
RCS file: /var/spool/cvs/mkd/flo/plugin/Admin/Content.pm,v
retrieving revision 1.1.2.9
retrieving revision 1.1.2.10
diff -Lflo/plugin/Admin/Content.pm -Lflo/plugin/Admin/Content.pm -u -r1.1.2.9 -r1.1.2.10
--- flo/plugin/Admin/Content.pm
+++ flo/plugin/Admin/Content.pm
@@ -163,6 +163,7 @@
my $document_t = flo::Standard::table ('Document');
my $document = flo::Standard::current_document();
+ $document->on_save();
$document->{Body} = $editor->generate_xml();
$document->set_date_last_modified ($document->now_iso());
$document->set_editor_last_modified (flo::Standard::current_user());
Index: DBH.pm
===================================================================
RCS file: /var/spool/cvs/mkd/lib/sql/Attic/DBH.pm,v
retrieving revision 1.2.2.1
retrieving revision 1.2.2.2
diff -Llib/sql/DBH.pm -Llib/sql/DBH.pm -u -r1.2.2.1 -r1.2.2.2
--- lib/sql/DBH.pm
+++ lib/sql/DBH.pm
@@ -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_;
--- /dev/null
+++ t/timerange.t
@@ -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'} = [];
+}
Index: en.html
===================================================================
RCS file: /var/spool/cvs/mkd/templates/component/headlines/Attic/en.html,v
retrieving revision 1.1.2.21
retrieving revision 1.1.2.22
diff -Ltemplates/component/headlines/en.html -Ltemplates/component/headlines/en.html -u -r1.1.2.21 -r1.1.2.22
--- templates/component/headlines/en.html
+++ templates/component/headlines/en.html
@@ -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,37 +185,84 @@
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>
<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">
@@ -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,29 +339,25 @@
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">
@@ -228,6 +365,8 @@
</small>
</dd>
</div>
+
</dl>
+
</div>
</div>
Index: en.html
===================================================================
RCS file: /var/spool/cvs/mkd/templates/editor/headlines/Attic/en.html,v
retrieving revision 1.1.2.12
retrieving revision 1.1.2.13
diff -Ltemplates/editor/headlines/en.html -Ltemplates/editor/headlines/en.html -u -r1.1.2.12 -r1.1.2.13
--- templates/editor/headlines/en.html
+++ templates/editor/headlines/en.html
@@ -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>
Index: en.xml
===================================================================
RCS file: /var/spool/cvs/mkd/templates/newsletter/Attic/en.xml,v
retrieving revision 1.1.2.6
retrieving revision 1.1.2.7
diff -Ltemplates/newsletter/en.xml -Ltemplates/newsletter/en.xml -u -r1.1.2.6 -r1.1.2.7
--- templates/newsletter/en.xml
+++ templates/newsletter/en.xml
@@ -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,18 +58,40 @@
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}
<${result/Full_Path}>
${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}
+ <${result/Full_Path}>
+${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: <${details/user_domain_notrailingslash}/${preferences/name}>"><http://example.com/preferences.mumble></span></pre>
+</span>
+
<p>
If you can't remember your account password, you can get an email
reminder by visiting this link:
Index: INSTALL.TXT
===================================================================
RCS file: /var/spool/cvs/mkd/INSTALL.TXT,v
retrieving revision 1.3.2.28
retrieving revision 1.3.2.29
diff -LINSTALL.TXT -LINSTALL.TXT -u -r1.3.2.28 -r1.3.2.29
--- INSTALL.TXT
+++ INSTALL.TXT
@@ -11,6 +11,18 @@
These instructions are suitable for the installation of a test version
of MKDoc, production servers may involve different configurations.
+Upgrading to MKDoc-1.6.30
+-------------------------
+
+If you are upgrading from a version of MKDoc prior to 1.6.30, you
+will need to create a MySQL Document_TimeRange table for each of
+your sites beforehand. You can do this by running the
+'timerange_update_db.pl' script for each site like so:
+
+ source /var/www/mkdoc/example.com/mksetenv.sh
+ perl /var/www/mkdoc/mkdoc-1.6/timerange_update_db.pl
+ etc..
+
Basic requirements
------------------
@@ -160,6 +172,8 @@
MKDoc::XML
MKDoc::Control_List
MKDoc::Text::Structured
+ DateTime
+ DateTime::TimeZone
Configuring your MKDoc system
--- /dev/null
+++ timerange_update_db.pl
@@ -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
Index: MODULES.TXT
===================================================================
RCS file: /var/spool/cvs/mkd/MODULES.TXT,v
retrieving revision 1.3.2.21
retrieving revision 1.3.2.22
diff -LMODULES.TXT -LMODULES.TXT -u -r1.3.2.21 -r1.3.2.22
--- MODULES.TXT
+++ MODULES.TXT
@@ -35,6 +35,8 @@
# Time & Date manipulation:
Date::Manip v5.52
Time::ParseDate
+ DateTime
+ DateTime::TimeZone
# Image manipulation:
Image::Magick v5.48
Index: Schema.pm
===================================================================
RCS file: /var/spool/cvs/mkd/MKDoc/Site/Deploy/DB/Schema.pm,v
retrieving revision 1.1.2.5
retrieving revision 1.1.2.6
diff -LMKDoc/Site/Deploy/DB/Schema.pm -LMKDoc/Site/Deploy/DB/Schema.pm -u -r1.1.2.5 -r1.1.2.6
--- MKDoc/Site/Deploy/DB/Schema.pm
+++ MKDoc/Site/Deploy/DB/Schema.pm
@@ -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__
Index: Document.pm
===================================================================
RCS file: /var/spool/cvs/mkd/flo/Record/Document.pm,v
retrieving revision 1.25.2.67
retrieving revision 1.25.2.68
diff -Lflo/Record/Document.pm -Lflo/Record/Document.pm -u -r1.25.2.67 -r1.25.2.68
--- flo/Record/Document.pm
+++ flo/Record/Document.pm
@@ -436,6 +436,9 @@
{
my $self = shift;
$self->is_root() and return;
+
+ # call delete handler
+ $self->on_delete();
$_->delete() for ($self->children());
$self->_delete_redirect (@_);
@@ -2591,5 +2594,33 @@
return $time;
}
+##
+# $self->on_save;
+#
+# Handler which gets called just before saving the document. Needed
+# to clean up index tables, like Document_TimeRange.
+##
+sub on_save {
+ my $self = shift;
+ my $id = $self->id();
+
+ # new documents won't need pre-save cleanup
+ return unless $id;
+
+ my $dbh = lib::sql::DBH->get();
+ $dbh->do('DELETE FROM Document_TimeRange WHERE Document_ID = ?',
+ undef, $id);
+}
+
+##
+# $self->on_delete;
+#
+# Handler which gets called just before deleting the document. Needed
+# to clean up index tables, like Document_TimeRange.
+##
+sub on_delete {
+ my $self = shift;
+ $self->on_save();
+}
1;
--- /dev/null
+++ flo/editor/TimeRange.pm
@@ -0,0 +1,525 @@
+# -------------------------------------------------------------------------------------
+# 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));
+
+ # insert a new record and get back the new 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;
Index: Headlines.pm
===================================================================
RCS file: /var/spool/cvs/mkd/flo/editor/Headlines.pm,v
retrieving revision 1.4.2.30
retrieving revision 1.4.2.31
diff -Lflo/editor/Headlines.pm -Lflo/editor/Headlines.pm -u -r1.4.2.30 -r1.4.2.31
--- flo/editor/Headlines.pm
+++ flo/editor/Headlines.pm
@@ -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');
@@ -320,12 +425,32 @@
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 +470,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,22 +484,52 @@
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;
};
Index: 020..newsletter.pl
===================================================================
RCS file: /var/spool/cvs/mkd/tools/cron/020..newsletter.pl,v
retrieving revision 1.1.2.16
retrieving revision 1.1.2.17
diff -Ltools/cron/020..newsletter.pl -Ltools/cron/020..newsletter.pl -u -r1.1.2.16 -r1.1.2.17
--- tools/cron/020..newsletter.pl
+++ tools/cron/020..newsletter.pl
@@ -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-commit
mailing list