Bug 7977: Quote-of-the-day (QOTD) Feature for OPAC
authorChris Nighswonger <cnighswonger@foundations.edu>
Mon, 9 Apr 2012 20:35:06 +0000 (16:35 -0400)
committerPaul Poulain <paul.poulain@biblibre.com>
Thu, 24 May 2012 12:14:05 +0000 (14:14 +0200)
This feature will add the option of displaying a select quote
for the day on the OPAC homepage. It will include the addition
of a QOTD editor in the tools section of the staff interface
which will allow the addition, editing, and deletion of quotes.
A single system preference will enable/disable the display of
the QOTD on the OPAC homepage. A new granular permission will
also be added to conrol user access to the QOTD editor tool.

Possible extentions to this would be code to allow alternate
quote selection algorithms to be added to vary how the currently
displayed quote is selected from the table of quotes.

This patch is a squash of the following work:

--Adding quotes table to kohastructure.sql and updatedatabase.pl

Note: This patch is intended for a MySQL based installation only.
That means that it includes backticks because that is what is required
at this point in history to install Koha correctly on a MySQL based
installation.

Feel free to port this over to the Pg stuff in the data/Pg directory.

--Adding system preference to control display of QOTD on OPAC main page
--Adding sample quote data

These quotes are taken from various US presidents. I'm not sure these
are applicable to the rest of the world, so I'm leaving it to translators
and others to add sample quote data for other languages.

--Adding edit_quotes user permissions
--Squash with other db related patches in this series
--Adding quotes editor pages to tools
--Adding QOTD editor link to tools homepage
--Integration of DataTables into QOTD Editor
--Impleminting jEditable into the quote editor
--Adding delete and add option to editor
--Fixing OPAC display so the QOTD div does not show if no quote is returned
--Also removing useless code from tools/quotes.pl
--Adding spans around QOTD foo in opac-main.tt
--Also fixing quote selection logic to accomodate the possibility of
    a single quote in the db a litte better.
--Changing timestamp column header to a more user-friendly 'Last Displayed'
--Fixing bug in quote selection logic. This bug caused a situation where
    when the table had only a single quote with an id greater than 1 in it,
    no quote would be selected.
--Fixing up sorting in the quote editor table. --jcamins
--Adding span element to quote separator --jcamins
--Overriding the default empty table message supplied by datatables-strings.inc
--Adds missing page heading
--Adds beginning of click-for-help elements.
--Refactors delete functionality to allow selecting of quotes to delete,
    enabling multi-delete.
--Refactors saving added quote functionality so that striking <Enter>
    saves the new quote.
--Refactors canceling aded quote functionality so that striking <Esc>
    cancels the new quote.
--Removing debug console.log statements
--Implementng the YUI button widget/toolbar used on other pages.
--Adds a 5px radius to all YUI buttons to bring them into
    conformity with the general trend toward rounded corners
--Fixes capitalization in quote editor
--Implements improvements suggested by jcamins and oleonard
--Adds DataTables Plugin dataTables.fnReloadAjax.js

Signed-off-by: Jared Camins-Esakov <jcamins@cpbibliography.com>
Signed-off-by: Mason James <mtj@kohaaloha.com>
27 files changed:
C4/Koha.pm
installer/data/mysql/de-DE/mandatory/userpermissions.sql
installer/data/mysql/en/mandatory/userpermissions.sql
installer/data/mysql/en/optional/sample_quotes.sql [new file with mode: 0644]
installer/data/mysql/en/optional/sample_quotes.txt [new file with mode: 0644]
installer/data/mysql/fr-FR/1-Obligatoire/userpermissions.sql
installer/data/mysql/it-IT/necessari/userpermissions.sql
installer/data/mysql/kohastructure.sql
installer/data/mysql/nb-NO/1-Obligatorisk/userpermissions.sql
installer/data/mysql/pl-PL/mandatory/userpermissions.sql
installer/data/mysql/ru-RU/mandatory/permissions_and_user_flags.sql
installer/data/mysql/sysprefs.sql
installer/data/mysql/uk-UA/mandatory/permissions_and_user_flags.sql
installer/data/mysql/updatedatabase.pl
koha-tmpl/intranet-tmpl/prog/en/css/datatables.css
koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
koha-tmpl/intranet-tmpl/prog/en/includes/quotes-toolbar.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc
koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/dataTables.fnReloadAjax.js [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
koha-tmpl/opac-tmpl/prog/en/css/opac.css
koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt
opac/opac-main.pl
tools/quotes.pl [new file with mode: 0755]
tools/quotes/quotes_ajax.pl [new file with mode: 0755]

index a028937..2bd1550 100644 (file)
@@ -22,10 +22,15 @@ package C4::Koha;
 
 use strict;
 #use warnings; FIXME - Bug 2505
+
 use C4::Context;
+
 use Memoize;
+use DateTime;
+use DateTime::Format::MySQL;
+use autouse 'Data::Dumper' => qw(Dumper);
 
-use vars qw($VERSION @ISA @EXPORT $DEBUG);
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $DEBUG);
 
 BEGIN {
        $VERSION = 3.01;
@@ -67,6 +72,7 @@ BEGIN {
                $DEBUG
        );
        $DEBUG = 0;
+@EXPORT_OK = qw( GetDailyQuote );
 }
 
 # expensive functions
@@ -1301,6 +1307,78 @@ sub GetNormalizedOCLCNumber {
     }
 }
 
+=head2 GetDailyQuote($opts)
+
+Takes a hashref of options
+
+Currently supported options are:
+
+'id'        An exact quote id
+'random'    Select a random quote
+noop        When no option is passed in, this sub will return the quote timestamped for the current day
+
+The function returns an anonymous hash following this format:
+
+        {
+          'source' => 'source-of-quote',
+          'timestamp' => 'timestamp-value',
+          'text' => 'text-of-quote',
+          'id' => 'quote-id'
+        };
+
+=cut
+
+# This is definitely a candidate for some sort of caching once we finally settle caching/persistence issues...
+# at least for default option
+
+sub GetDailyQuote {
+    my %opts = @_;
+    my $dbh = C4::Context->dbh;
+    my $query = '';
+    my $sth = undef;
+    my $quote = undef;
+    if ($opts{'id'}) {
+        $query = 'SELECT * FROM quotes WHERE id = ?';
+        $sth = $dbh->prepare($query);
+        $sth->execute($opts{'id'});
+        $quote = $sth->fetchrow_hashref();
+    }
+    elsif ($opts{'random'}) {
+        # Fall through... we also return a random quote as a catch-all if all else fails
+    }
+    else {
+        $query = 'SELECT * FROM quotes WHERE timestamp LIKE CONCAT(CURRENT_DATE,\'%\') ORDER BY timestamp LIMIT 0,1';
+        $sth = $dbh->prepare($query);
+        $sth->execute();
+        $quote = $sth->fetchrow_hashref();
+    }
+    unless ($quote) {        # if there are not matches, choose a random quote
+        # get a list of all available quote ids
+        $sth = C4::Context->dbh->prepare('SELECT count(*) FROM quotes;');
+        $sth->execute;
+        my $range = ($sth->fetchrow_array)[0];
+        if ($range > 1) {
+            # chose a random id within that range if there is more than one quote
+            my $id = int(rand($range));
+            # grab it
+            $query = 'SELECT * FROM quotes WHERE id = ?;';
+            $sth = C4::Context->dbh->prepare($query);
+            $sth->execute($id);
+        }
+        else {
+            $query = 'SELECT * FROM quotes;';
+            $sth = C4::Context->dbh->prepare($query);
+            $sth->execute();
+        }
+        $quote = $sth->fetchrow_hashref();
+        # update the timestamp for that quote
+        $query = 'UPDATE quotes SET timestamp = ? WHERE id = ?';
+        $sth = C4::Context->dbh->prepare($query);
+        $sth->execute(DateTime::Format::MySQL->format_datetime(DateTime->now), $quote->{'id'});
+    }
+    return $quote;
+}
+
 sub _normalize_match_point {
     my $match_point = shift;
     (my $normalized_match_point) = $match_point =~ /([\d-]*[X]*)/;
index 254e090..54fd531 100644 (file)
@@ -22,6 +22,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Benutzerkommentare moderieren'),
    (13, 'edit_notices', 'Benachrichtigungen verwalten'),
    (13, 'edit_notice_status_triggers', 'Mahntrigger für überfällige Medien verwalten'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Logs durchsuchen/einsehen'),
    (13, 'inventory', 'Inventur durchführen'),
    (13, 'stage_marc_import', 'MARC-Datensätze zwischenspeichern'),
index da61f69..70f89e2 100644 (file)
@@ -22,6 +22,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Moderate patron comments'),
    (13, 'edit_notices', 'Define notices'),
    (13, 'edit_notice_status_triggers', 'Set notice/status triggers for overdue items'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Browse the system logs'),
    (13, 'inventory', 'Perform inventory (stocktaking) of your catalog'),
    (13, 'stage_marc_import', 'Stage MARC records into the reservoir'),
diff --git a/installer/data/mysql/en/optional/sample_quotes.sql b/installer/data/mysql/en/optional/sample_quotes.sql
new file mode 100644 (file)
index 0000000..9a14e32
--- /dev/null
@@ -0,0 +1,34 @@
+/*!40000 ALTER TABLE `quotes` DISABLE KEYS */;
+INSERT INTO `quotes` VALUES
+(1,'George Washington','To be prepared for war is one of the most effectual means of preserving peace.','0000-00-00 00:00:00'),
+(2,'Thomas Jefferson','When angry, count ten, before you speak; if very angry, an hundred.','0000-00-00 00:00:00'),
+(3,'Abraham Lincoln','Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.','2012-04-12 20:30:59'),
+(4,'Abraham Lincoln','I have always found that mercy bears richer fruits than strict justice.','0000-00-00 00:00:00'),
+(5,'Andrew Johnson','I feel incompetent to perform duties...which have been so unexpectedly thrown upon me.','0000-00-00 00:00:00'),
+(6,'Rutherford B. Hayes','He serves his party best who serves his country best.','0000-00-00 00:00:00'),
+(7,'Theodore Roosevelt','No President has ever enjoyed himself as much as I?','0000-00-00 00:00:00'),
+(8,'Theodore Roosevelt','Speak softly and carry a big stick.','0000-00-00 00:00:00'),
+(9,'William H. Taft','I have come to the conclusion that the major part of the president is to increase the gate receipts of expositions and fairs and bring tourists to town.','0000-00-00 00:00:00'),
+(11,'Woodrow Wilson','You cannot become thorough Americans if you think of yourselves in groups. America does not consist of groups. A man who thinks of himself as belonging to a particular national group in America has not yet become an American.','0000-00-00 00:00:00'),
+(12,'Calvin Coolidge','If you don\'t say anything, you won\'t be called on to repeat it.','0000-00-00 00:00:00'),
+(13,'Calvin Coolidge','Four fifths of all our troubles in this life would disappear if we would only sit down and keep still.','0000-00-00 00:00:00'),
+(14,'Herbert Hoover','The slogan of progress is changing from the full dinner pail to the full garage.','0000-00-00 00:00:00'),
+(15,'Franklin D. Roosevelt','Let me assert my firm belief that the only thing we have to fear is fear itself.','0000-00-00 00:00:00'),
+(16,'Harry S. Truman','If you can\'t stand the heat, get out of the kitchen.','0000-00-00 00:00:00'),
+(17,'Harry S. Truman','The atom bomb was no great decision. It was merely another powerful weapon in the arsenal of righteousness.','0000-00-00 00:00:00'),
+(18,'Harry S. Truman','The buck stops here.','0000-00-00 00:00:00'),
+(19,'Dwight D. Eisenhower','History does not long entrust the care of freedom to the weak or the timid.','0000-00-00 00:00:00'),
+(20,'John F. Kennedy','My fellow Americans, ask not what your country can do for you; ask what you can do for your country.','0000-00-00 00:00:00'),
+(22,'John F. Kennedy','I do not think it altogether inappropriate to introduce myself to this audience. I am the man who accompanied Jacqueline Kennedy to Paris, and I have enjoyed it.','0000-00-00 00:00:00'),
+(23,'Lyndon B. Johnson','The Great Society is a place where every child can find knowledge to enrich his mind and to enlarge his talents. It is a place where the city of man serves not only the needs of the body and the demands of commerce but the desire for beauty and the hunger for community. It is a place where men are more concerned with the quality of their goals than the quantity of their goods.','0000-00-00 00:00:00'),
+(24,'Richard Nixon','People have got to know whether or not their president is a crook. Well, I\'m not a crook.','0000-00-00 00:00:00'),
+(25,'Richard Nixon','When the President does it, that means that it is not illegal.','0000-00-00 00:00:00'),
+(26,'Gerald R. Ford','Our Long national nightmare is over.','2012-04-18 19:42:14'),
+(27,'Gerald R. Ford','Our constitution works. Our great republic is a government of laws, not of men.','0000-00-00 00:00:00'),
+(28,'Jimmy Carter','A strong nation, like a strong person, can afford to be gentle, firm, thoughtful, and restrained. It can afford to extend a helping hand to others. It\'s a weak nation, like a weak person, that must behave with bluster and boasting and rashness and other signs of insecurity.','0000-00-00 00:00:00'),
+(30,'Ronald Reagan','It is not my intention to do away with government. It is rather to make it work -work with us, not over us; stand by our side, not ride on our back. Government can and must provide opportunity, not smother it; foster productivity, not stifle it. This Administration\'s objective will be a healthy, vigorous, growing economy.','0000-00-00 00:00:00'),
+(31,'Ronald Reagan','Each generation goes further than the generation preceding it because it stands on the shoulders of that generation. You will have opportunities beyond anything we\'ve ever known.','0000-00-00 00:00:00'),
+(32,'George H. W. Bush ','We have ...drawn a line in the sand.','0000-00-00 00:00:00'),
+(33,'George W. Bush','If America shows weakness and uncertainty, the world will drift toward tragedy. That will not happen on my watch.','0000-00-00 00:00:00'),
+(34,'George W. Bush','Every nation in every region now has a decision to make. Either you are with us, or you are with the terrorists.','0000-00-00 00:00:00');
+/*!40000 ALTER TABLE `quotes` ENABLE KEYS */;
diff --git a/installer/data/mysql/en/optional/sample_quotes.txt b/installer/data/mysql/en/optional/sample_quotes.txt
new file mode 100644 (file)
index 0000000..b8b2c2d
--- /dev/null
@@ -0,0 +1 @@
+Sample Quotes
index 7a30f82..d6362e8 100644 (file)
@@ -12,6 +12,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Modérer les commentaires des adhérents'),
    (13, 'edit_notices', 'Définir les notifications'),
    (13, 'edit_notice_status_triggers', 'Définir les déclencheurs de notification de retard'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Parcourir les journaux de l''activité du système'),
    (13, 'inventory', 'Réaliser les tâches de récolement'),
    (13, 'stage_marc_import', 'Importer des notices MARC dans le réservoir'),
index 0e12644..03dd32c 100644 (file)
@@ -24,6 +24,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Modera i commenti degli utenti'),
    (13, 'edit_notices', 'Definisci le notifiche'),
    (13, 'edit_notice_status_triggers', 'Imposta il messaggio o lo stato delle notifiche per le copie in ritardo'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Scorri i log di sistema'),
    (13, 'inventory', 'Lavora sugli inventari (stocktaking) del tuo catalogo'),
    (13, 'stage_marc_import', 'Opera sui Record MARC presenti nella zona di lavoro'),
index 9a94e0f..a6f8456 100644 (file)
@@ -2831,6 +2831,19 @@ CREATE TABLE ratings (
     CONSTRAINT ratings_ibfk_2 FOREIGN KEY (biblionumber) REFERENCES biblio (biblionumber) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+--
+-- Table structure for table `quotes`
+--
+
+DROP TABLE IF EXISTS quotes;
+CREATE TABLE `quotes` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `source` varchar(45) DEFAULT NULL,
+  `text` mediumtext NOT NULL,
+  `timestamp` datetime NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8
+
 /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
index f10af31..fd3989d 100644 (file)
@@ -43,6 +43,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Behandle kommentarer fra lånere'),
    (13, 'edit_notices', 'Definere meldinger'),
    (13, 'edit_notice_status_triggers', 'Definere utløsere for meldinger og statusenderinger for for sent leverte dokumenter'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Se Koha sine logger'),
    (13, 'inventory', 'Foreta varetelling'),
    (13, 'stage_marc_import', 'Importere MARC-poster til brønnen'),
index a46ddfe..a9629c5 100644 (file)
@@ -22,6 +22,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments', 'Moderate patron comments'),
    (13, 'edit_notices', 'Define notices'),
    (13, 'edit_notice_status_triggers', 'Set notice/status triggers for overdue items'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs', 'Przeglądanie logów systemowych'),
    (13, 'inventory', 'Perform inventory (stocktaking) of your catalog'),
    (13, 'stage_marc_import', 'Stage MARC records into the reservoir'),
index d948863..07b1545 100644 (file)
@@ -46,6 +46,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments',           'Регулировка комментариев от посетителей'),
    (13, 'edit_notices',                'Определение сообщений'),
    (13, 'edit_notice_status_triggers', 'Установка триггеров сообщений/статусов для просроченных экземпляров'),
+   (13, 'edit_quotes',                 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs',            'Просмотр протоколов системы'),
    (13, 'inventory',                   'Проведение инвентаризации(анализа) Вашего каталога'),
    (13, 'stage_marc_import',           'Заготовка МАРК-записей в хранилище'),
index 1edf08e..12928cc 100644 (file)
@@ -95,6 +95,7 @@ INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('OpacPublic',1,'Turn on/off public OPAC',NULL,'YesNo');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('opacuserjs','','Define custom javascript for inclusion in OPAC','70|10','Textarea');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('opacuserlogin',1,'Enable or disable display of user login features',NULL,'YesNo');
+INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('QuoteOfTheDay',0,'Enable or disable display of Quote of the Day on the OPAC home page',NULL,'YesNo');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('patronimages',0,'Enable patron images for the Staff Client',NULL,'YesNo');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('OPACpatronimages',0,'Enable patron images in the OPAC',NULL,'YesNo');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('printcirculationslips',1,'If ON, enable printing circulation receipts','','YesNo');
index 53368b6..40034cf 100644 (file)
@@ -46,6 +46,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'moderate_comments',           'Регулювання коментарів від відвідувачів'),
    (13, 'edit_notices',                'Визначення повідомлень'),
    (13, 'edit_notice_status_triggers', 'Встановлення тригерів повідомлень/статусів для прострочених примірників'),
+   (13, 'edit_quotes', 'Edit quotes for quote-of-the-day feature'),
    (13, 'view_system_logs',            'Перегляд протоколів системи'),
    (13, 'inventory',                   'Проведення інвентаризації(аналізу) Вашого каталогу'),
    (13, 'stage_marc_import',           'Заготівля МАРК-записів у сховище'),
index 38aa70c..500e25a 100755 (executable)
@@ -5246,6 +5246,29 @@ if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
     SetVersion($DBversion);
 }
 
+$DBversion = "3.09.00.XXX";
+if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
+    unless (TableExists('quotes')) {
+        $dbh->do( qq{
+            CREATE TABLE `quotes` (
+              `id` int(11) NOT NULL AUTO_INCREMENT,
+              `source` varchar(45) DEFAULT NULL,
+              `text` mediumtext NOT NULL,
+              `timestamp` datetime NOT NULL,
+              PRIMARY KEY (`id`)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+        });
+    }
+    $dbh->do( qq{
+        INSERT IGNORE INTO permissions VALUES (13, "edit_quotes","Edit quotes for quote-of-the-day feature");
+    });
+    $dbh->do( qq{
+        INSERT IGNORE INTO `systempreferences` (variable,value,explanation,options,type) VALUES('QuoteOfTheDay',0,'Enable or disable display of Quote of the Day on the OPAC home page',NULL,'YesNo');
+    });
+    print "Upgrade to $DBversion done (Adding Quote of the Day Option.)\n";
+    SetVersion($DBversion);
+}
+
 =head1 FUNCTIONS
 
 =head2 TableExists($table)
index ba7e233..2a78678 100644 (file)
@@ -85,25 +85,25 @@ div.dataTables_paginate {
 .paging_full_numbers a.paginate_button.first {
     background-image : url('../../img/first.png');
     background-repeat: no-repeat;
-    background-position : 2px center;
+    background-position : 1% center;
     padding-left : 2em;
 }
 .paging_full_numbers a.paginate_button.previous {
     background-image : url('../../img/prev.png');
     background-repeat: no-repeat;
-    background-position : 2px center;
+    background-position : 1% center;
     padding-left : 2em;
 }
 .paging_full_numbers a.paginate_button.next {
     background-image : url('../../img/next.png');
     background-repeat: no-repeat;
-    background-position : right center;
+    background-position : 96% center;
     padding-right : 2em;
 }
 .paging_full_numbers a.paginate_button.last {
     background-image : url('../../img/last.png');
     background-repeat: no-repeat;
-    background-position : right center;
+    background-position : 96% center;
     border-right : 1px solid #686868;
     padding-right : 2em;
 }
@@ -175,6 +175,21 @@ div.dataTables_paginate.paging_four_button {
     cursor: pointer;
 }
 
+.dataTables_processing {
+    background-color: white;
+    border: 1px solid #DDDDDD;
+    color: #999999;
+    font-size: 14px;
+    height: 30px;
+    left: 50%;
+    margin-left: -125px;
+    margin-top: -15px;
+    padding: 14px 0 2px;
+    position: fixed;
+    text-align: center;
+    top: 50%;
+    width: 250px;
+}
 
 /*
 table.display {
index 0835187..a05b6e5 100644 (file)
@@ -26,6 +26,14 @@ a:hover, a:active {
     text-decoration: none;
 }
 
+.yui-button {
+    border-radius: 5px;
+}
+
+.yui-button .first-child {
+    border-radius: 5px;
+}
+
 .yui-button,.yui-button a:link,.yui-button a:visited {
        color : #000;
 }
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/quotes-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/quotes-toolbar.inc
new file mode 100644 (file)
index 0000000..7372a70
--- /dev/null
@@ -0,0 +1,48 @@
+<script type="text/javascript">
+    //<![CDATA[
+    // prepare DOM for YUI Toolbar
+    $(document).ready(function() {
+        $("#add_quote").empty();
+        $("#delete_quote").empty();
+        $("#import_quotes").empty();
+        yuiToolbar();
+     });
+
+    // YUI Toolbar Functions
+    function yuiToolbar() {
+
+        new YAHOO.widget.Button({
+            type: "button",
+            label: _("Add quote"),
+            name: "add",
+            container: "add_quote",
+            onclick: {fn: fnClickAddRow}
+        });
+
+        new YAHOO.widget.Button({
+            type: "button",
+            label: _("Delete quote(s)"),
+            name: "delete",
+            container: "delete_quote",
+            onclick: {fn: fnClickDeleteRow}
+        });
+
+        new YAHOO.widget.Button({
+            type: "button",
+            label: _("Import quotes"),
+            name: "import",
+            container: "import_quotes",
+            onclick: {fn: function (){parent.location="quotes-upload.pl";}}
+        });
+    }
+
+//]]>
+</script>
+<div id="toolbar">
+    <ul class="toolbar">
+        <li id="add_quote"><a id="add" href="#">Add quote</a></li>
+        <li id="delete_quote"><a id="delete" href="#">Delete quote(s)</a></li>
+        <li id="import_quotes"><a id="import" href="#">Import quotes</a></li>
+        <span class="hint" style="">Click Source or Text field to edit contents. Press &lt;Enter&gt; to save changes.</span>
+    </ul>
+</div>
index 5b6c7cd..b70ffd4 100644 (file)
@@ -91,4 +91,7 @@
     [% IF ( CAN_user_tools_schedule_tasks ) %]
        <li><a href="/cgi-bin/koha/tools/scheduler.pl">Task scheduler</a></li>
     [% END %]
+    [% IF ( CAN_user_tools_edit_quotes ) %]
+       <li><a href="/cgi-bin/koha/tools/quotes.pl">Quote editor</a></li>
+    [% END %]
 </ul></div></div>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/dataTables.fnReloadAjax.js b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/dataTables.fnReloadAjax.js
new file mode 100644 (file)
index 0000000..0eac52a
--- /dev/null
@@ -0,0 +1,50 @@
+$.fn.dataTableExt.oApi.fnReloadAjax = function ( oSettings, sNewSource, fnCallback, bStandingRedraw )
+{
+    if ( typeof sNewSource != 'undefined' && sNewSource != null )
+    {
+        oSettings.sAjaxSource = sNewSource;
+    }
+    this.oApi._fnProcessingDisplay( oSettings, true );
+    var that = this;
+    var iStart = oSettings._iDisplayStart;
+    var aData = [];
+
+    this.oApi._fnServerParams( oSettings, aData );
+
+    oSettings.fnServerData( oSettings.sAjaxSource, aData, function(json) {
+        /* Clear the old information from the table */
+        that.oApi._fnClearTable( oSettings );
+
+        /* Got the data - add it to the table */
+        var aData =  (oSettings.sAjaxDataProp !== "") ?
+            that.oApi._fnGetObjectDataFn( oSettings.sAjaxDataProp )( json ) : json;
+
+        for ( var i=0 ; i<aData.length ; i++ )
+        {
+            that.oApi._fnAddData( oSettings, aData[i] );
+        }
+
+        oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
+        that.fnDraw();
+
+        if ( typeof bStandingRedraw != 'undefined' && bStandingRedraw === true )
+        {
+            oSettings._iDisplayStart = iStart;
+            that.fnDraw( false );
+        }
+
+        that.oApi._fnProcessingDisplay( oSettings, false );
+
+        /* Callback user function - for event handlers etc */
+        if ( typeof fnCallback == 'function' && fnCallback != null )
+        {
+            fnCallback( oSettings );
+        }
+    }, oSettings );
+}
+
+/* Example call to load a new file */
+//oTable.fnReloadAjax( 'media/examples_support/json_source2.txt' );
+
+/* Example call to reload from original file */
+//oTable.fnReloadAjax();
index eb74d81..89d3c42 100644 (file)
@@ -330,7 +330,13 @@ OPAC:
                   yes: Enable
                   no: Disable
             - browsing and paging search results from the OPAC detail page.
-
+        -
+            - pref: QuoteOfTheDay
+              default: 0
+              choices:
+                  yes: Enable
+                  no: Disable
+            - Quote of the Day display on OPAC home page
     Policy:
         -
             - pref: singleBranchMode
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/quotes.tt
new file mode 100644 (file)
index 0000000..7d020b5
--- /dev/null
@@ -0,0 +1,205 @@
+    [% INCLUDE 'doc-head-open.inc' %]
+    <title>Koha &rsaquo; Tools &rsaquo; Quote editor</title>
+    [% INCLUDE 'doc-head-close.inc' %]
+    <link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
+    <script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.dataTables.min.js"></script>
+    <script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/dataTables.fnReloadAjax.js"></script>
+    [% INCLUDE 'datatables-strings.inc' %]
+    </script>
+    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/datatables.js"></script>
+    <script type="text/javascript" src="/intranet-tmpl/prog/en/js/jquery.jeditable.mini.js"></script>
+    <script type="text/javascript">
+    //<![CDATA[
+    var oTable; /* oTable needs to be global */
+    var sEmptyTable = _('No quotes available. Please use the \"Add Quote\" button to add a quote.'); /* override the default message in datatables-strings.inc */
+    $(document).ready(function() {
+        /* NOTE: This is an ajax-source datatable and *not* a server-side sourced datatable. */
+        /* See the datatable docs if you don't understand this difference. */
+        oTable = $("#quotes_editor").dataTable({
+                    "bAutoWidth"        : false,
+                    "bProcessing"       : true,
+                    "bPaginate"         : true,
+                    "sPaginationType"   : "full_numbers",
+                    "sAjaxSource"       : "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
+                    "aoColumns"         : [
+                                            { "sWidth": "3%"  },
+                                            { "sWidth": "11%" },
+                                            { "sWidth": "75%" },
+                                            { "sWidth": "11%" },
+                                          ],
+                   "oLanguage"          : {
+                                            "sEmptyTable": sEmptyTable,
+                                          },
+                   "fnPreDrawCallback": function(oSettings) {
+                        return true;
+                    },
+                    "fnRowCallback": function( nRow, aData, iDisplayIndex ) {
+                        /* do foo on the current row and its child nodes */
+                        var noEditFields = [];
+                        var quoteID = $('td', nRow)[0].innerHTML;
+                        $(nRow).attr("id", quoteID); /* set row ids to quote id */
+                        $('td:eq(0)', nRow).click(function() {$(this.parentNode).toggleClass('selected',this.clicked);}); /* add row selectors */
+                        $('td:eq(0)', nRow).attr("title", "Click ID to select/deselect quote");
+                        if (isNaN(quoteID)) {
+                            noEditFields = [0,1,2,3]; /* all fields when adding a quote */
+                        }
+                        else {
+                            noEditFields = [0,3]; /* id, timestamp */
+                        }
+                        /* apply no_edit id to noEditFields */
+                        for (i=0; i<noEditFields.length; i++) {
+                            $('td', nRow)[noEditFields[i]].setAttribute("id","no_edit");
+                        }
+                        return nRow;
+                    },
+                   "fnDrawCallback": function(oSettings) {
+                        /* Apply the jEditable handlers to the table on all fields w/o the no_edit id */
+                        $('#quotes_editor tbody td[id!="no_edit"]').editable( "/cgi-bin/koha/tools/quotes/quotes_ajax.pl", {
+                            "submitdata"    : function ( value, settings ) {
+                                                  return {
+                                                      "column"        : oTable.fnGetPosition( this )[2],
+                                                      "action"        : "edit",
+                                                  };
+                                              },
+                            "height"        : "14px",
+                            "placeholder"   : "Saving data...",
+                        });
+                   },
+        });
+    });
+
+        function fnClickAddQuote(e, node) {
+            if (e.charCode) {
+                /* some browsers only give charCode, so this will need to be */
+                /* fixed up to handle that */
+                console.log('charCode: '+e.charCode);
+            }
+            if (e.keyCode == 13) {
+                var quoteSource = $('#quoteSource').val();
+                var quoteText = $('#quoteText').val()
+                /* If passed a quote source, add the quote to the db */
+                if (quoteSource && quoteText) {
+                    $.ajax({
+                        url: "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
+                        type: "POST",
+                        data: {
+                                "source"    : quoteSource,
+                                "text"      : quoteText,
+                                "action"    : "add",
+                        },
+                        success: function(data){
+                                    var newQuote = data[0];
+                                    var aRow = oTable.fnUpdate(
+                                        newQuote,
+                                        node,
+                                        false,
+                                        false
+                                    );
+                                    oTable.fnPageChange( 'last' );
+                                    $('.add_quote_button').attr('onclick', 'fnClickAddRow()'); // re-enable add button
+                            }
+                    });
+                }
+                else {
+                    alert('Please supply both the text and source of the quote before saving.');
+                }
+            }
+            else if (e.keyCode == 27) {
+                if (confirm('Are you sure you want to cancel adding this quote?')) {
+                    oTable.fnDeleteRow(node);
+                }
+                else {
+                    return;
+                }
+            }
+        }
+
+        function fnClickAddRow() {
+            $('.add_quote_button').removeAttr('onclick'); // disable add button once it has been clicked
+            var aRow = oTable.fnAddData(
+                [
+                    'NA', // this is hackish to fool the datatable sort routine into placing this row at the end of the list...
+                    '<input id="quoteSource" type="text" style="height:14px; width:99%" onkeydown="fnClickAddQuote(event,this.parentNode.parentNode)"/>',
+                    '<input id="quoteText" type="text" style="height:14px; width:99%" onkeydown="fnClickAddQuote(event,this.parentNode.parentNode)"/>',
+                    '0000-00-00 00:00:00',
+                ],
+                false
+            );
+            oTable.fnPageChange( 'last' );
+            $('#quoteSource').focus();
+        }
+
+        function fnClickDeleteRow() {
+            var idsToDelete = oTable.$('.selected').map(function() {
+                  return this.id;
+            }).get().join(', ');
+            if (!idsToDelete) {
+                alert('Please select a quote(s) by clicking the quote id(s) you desire to delete.');
+            }
+            else if (confirm('Are you sure you wish to delete quote(s) '+idsToDelete+'?')) {
+                oTable.$('.selected').each(function(){
+                        var quoteID = $(this).attr('id');
+                            $.ajax({
+                                url: "/cgi-bin/koha/tools/quotes/quotes_ajax.pl",
+                                type: "POST",
+                                data: {
+                                        "id"        : quoteID,
+                                        "action"    : "delete",
+                                },
+                                /* Delete the row from the datatable */
+                                success: function(){
+                                    oTable.fnDeleteRow(this);
+                                    oTable.fnReloadAjax(null, null, true);
+                                }
+                            });
+                    });
+            }
+            else {
+                return;
+            }
+        }
+    //]]>
+    </script>
+</head>
+<body id="tools_quotes" class="tools">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> &rsaquo; Quote Editor</div>
+
+<div id="doc3" class="yui-t2">
+    <div id="bd">
+        <div id="yui-main">
+            <div class="yui-b">
+                [% INCLUDE 'quotes-toolbar.inc' %]
+                <h2>Quote editor</h2>
+                <table id="quotes_editor" style="float: left; width: 100%;">
+                <thead>
+                    <tr>
+                        <th><span style="cursor: help" onclick="event.stopPropagation();alert('Click on the quote\'s id to select or deselect the quote. Multiple quotes may be selected.');">ID</span></th>
+                        <th>Source</th>
+                        <th>Text</th>
+                        <th>Last Displayed</th>
+<!--                        <th>Actions</th>-->
+                    </tr>
+                </thead>
+                <tbody>
+                    <!-- tbody content is generated by DataTables -->
+                    <tr>
+                        <td></td>
+                        <td></td>
+                        <td>Loading data...</td>
+                        <td></td>
+<!--                        <td></td>-->
+                    </tr>
+                </tbody>
+                </table>
+                <fieldset id="footer" class="action" style="height:25px;">
+                </fieldset>
+            </div>
+        </div>
+    <div class="yui-b noprint">
+        [% INCLUDE 'tools-menu.inc' %]
+    </div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
index 4a91c10..3806965 100644 (file)
     <dd>Schedule tasks to run</dd>
     [% END %]
        
+    [% IF ( CAN_user_tools_edit_quotes ) %]
+    <dt><a href="/cgi-bin/koha/tools/quotes.pl">Edit Quotes for QOTD Feature</a></dt>
+    <dd>Quote editor for Quote-of-the-day feature in OPAC</dd>
+    [% END %]
 
 </dl>
 </div>
index 0cc7f2d..f0f9fac 100644 (file)
@@ -2405,6 +2405,29 @@ span.sep {
        text-shadow: 1px 1px 0 #FFF;
 }
 
+#daily-quote {
+    /*border-top : 1px solid #000000;*/
+    border : 1px solid #000000;
+    margin-top: 2px;
+    margin-bottom: 10px;
+    margin-left: 2px;
+    margin-right: 2px;
+    width: 300px;
+    text-align: center;
+    font-family: "Georgia","Palatino","Times New Roman",sans-serif;
+}
+
+#daily-quote h1 {
+    font-size: 18px;
+    font-weight: normal;
+    margin: 0;
+}
+
+#daily-quote div {
+    font-size: 12px;
+    margin: 5px;
+}
+
 /* ## BABELTHEQUE ## */
 /* Uncomment if babeltheque configuration no contains these lines */
 /*
index d2d68c5..a89c473 100644 (file)
 </div>
 [% END %]
 
+      [% IF ( display_daily_quote && daily_quote ) %]
+    <div id="daily-quote" class="container"><h1>Quote of the Day</h1><div><span id="daily-quote-text">[% daily_quote.text %]</span><span id="daily-quote-sep"> ~ </span><span id="daily-quote-source">[% daily_quote.source %]</span></div></div>
+[% END %]
+
        [% IF ( OpacMainUserBlock ) %]<div id="opacmainuserblock" class="container">[% OpacMainUserBlock %]</div>[% END %]
 
 </div>
index 5d6c2e1..78ee2e2 100755 (executable)
@@ -23,6 +23,7 @@ use C4::Auth;    # get_template_and_user
 use C4::Output;
 use C4::NewsChannels;    # get_opac_news
 use C4::Languages qw(getTranslatedLanguages accept_language);
+use C4::Koha qw( GetDailyQuote );
 
 my $input = new CGI;
 my $dbh   = C4::Context->dbh;
@@ -50,9 +51,13 @@ my ($theme, $news_lang) = C4::Templates::themelanguage(C4::Context->config('opac
 my $all_koha_news   = &GetNewsToDisplay($news_lang);
 my $koha_news_count = scalar @$all_koha_news;
 
+my $quote = GetDailyQuote();   # other options are to pass in an exact quote id or select a random quote each pass... see perldoc C4::Koha
+
 $template->param(
-    koha_news       => $all_koha_news,
-    koha_news_count => $koha_news_count
+    koha_news           => $all_koha_news,
+    koha_news_count     => $koha_news_count,
+    display_daily_quote => C4::Context->preference('QuoteOfTheDay'),
+    daily_quote         => $quote,
 );
 
 # If GoogleIndicTransliteration system preference is On Set paramter to load Google's javascript in OPAC search screens
diff --git a/tools/quotes.pl b/tools/quotes.pl
new file mode 100755 (executable)
index 0000000..e12f4bf
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+
+# Copyright 2012 Foundations Bible College Inc.
+#
+# This file is part of Koha.
+#
+# Koha 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.
+#
+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use autouse 'Data::Dumper' => qw(Dumper);
+
+use C4::Auth;
+use C4::Koha;
+use C4::Context;
+use C4::Output;
+
+my $cgi = new CGI;
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "tools/quotes.tt",
+        query           => $cgi,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { tools => 'edit_quotes' },
+        debug           => 1,
+    }
+);
+
+output_html_with_http_headers $cgi, $cookie, $template->output;
diff --git a/tools/quotes/quotes_ajax.pl b/tools/quotes/quotes_ajax.pl
new file mode 100755 (executable)
index 0000000..eb54550
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/perl
+
+# Copyright 2012 Foundations Bible College Inc.
+#
+# This file is part of Koha.
+#
+# Koha 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.
+#
+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use JSON;
+use autouse 'Data::Dumper' => qw(Dumper);
+
+use C4::Auth;
+use C4::Context;
+
+my $cgi = CGI->new;
+my $dbh = C4::Context->dbh;
+my $sort_columns = ["id", "source", "text", "timestamp"];
+
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "",
+        query           => $cgi,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { tools => 'edit_quotes' },
+        debug           => 1,
+    }
+);
+
+# NOTE: This is a collection of ajax functions for use with tools/quotes.pl
+
+my $params = $cgi->Vars; # NOTE: Multivalue parameters NOT allowed!!
+
+print $cgi->header('application/json');
+
+if ($params->{'action'} eq 'add') {
+    my $sth = $dbh->prepare('INSERT INTO quotes (source, text) VALUES (?, ?);');
+    $sth->execute($params->{'source'}, $params->{'text'});
+    if ($sth->err) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        exit 0;
+    }
+    my $new_quote_id = $dbh->{q{mysql_insertid}}; # ALERT: mysqlism here
+    $sth = $dbh->prepare('SELECT * FROM quotes WHERE id = ?;');
+    $sth->execute($new_quote_id);
+    print to_json($sth->fetchall_arrayref);
+    exit 1;
+}
+elsif ($params->{'action'} eq 'edit') {
+    my $editable_columns = [qw(source text)]; # pay attention to element order; these columns match the quotes table columns
+    my $sth = $dbh->prepare("UPDATE quotes SET $editable_columns->[$params->{'column'}-1]  = ? WHERE id = ?;");
+    $sth->execute($params->{'value'}, $params->{'id'});
+    if ($sth->err) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        exit 0;
+    }
+    print $sth->fetchrow_array();
+    exit 1;
+}
+elsif ($params->{'action'} eq 'delete') {
+    my $sth = $dbh->prepare("DELETE FROM quotes WHERE id = ?;");
+    $sth->execute($params->{'id'});
+    if ($sth->err) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        exit 0;
+    }
+    exit 1;
+}
+else {
+    my $aaData = [];
+    my $iTotalRecords = '';
+    my $sth = '';
+
+    if (my $filter = $params->{'sSearch'}) {
+        # This seems a bit brute force and ungraceful, but it provides a very general, simple search on all fields
+        my $like = " id LIKE \"%$filter%\" OR source LIKE \"%$filter%\" OR text LIKE \"%$filter%\" OR timestamp LIKE \"%$filter%\"";
+        $iTotalRecords = $dbh->selectrow_array("SELECT count(*) FROM quotes WHERE $like;");
+        $sth = $dbh->prepare("SELECT * FROM quotes;");
+    }
+    else {
+        $iTotalRecords = $dbh->selectrow_array('SELECT count(*) FROM quotes;');
+        $sth = $dbh->prepare("SELECT * FROM quotes;");
+    }
+
+    $sth->execute();
+    if ($sth->err) {
+        warn sprintf('Database returned the following error: %s', $sth->errstr);
+        exit 0;
+    }
+
+    $aaData = $sth->fetchall_arrayref;
+    my $iTotalDisplayRecords = $iTotalRecords; # no filtering happening here
+
+
+    print to_json({
+                    iTotalRecords       =>  $iTotalRecords,
+                    iTotalDisplayRecords=>  $iTotalDisplayRecords,
+                    sEcho               =>  $params->{'sEcho'},
+                    aaData              =>  $aaData,
+                  });
+}