diff --git a/configure.ac b/configure.ac index 9cce278beb..1d23eca507 100644 --- a/configure.ac +++ b/configure.ac @@ -874,6 +874,10 @@ AC_CONFIG_FILES([config.nice \ modules/webmessage/doc/hacking/Makefile \ modules/webmessage/lib/Makefile \ modules/webmessage/web/Makefile \ + modules/webnews/Makefile \ + modules/webnews/doc/Makefile \ + modules/webnews/lib/Makefile \ + modules/webnews/web/Makefile \ modules/websearch/Makefile \ modules/websearch/bin/Makefile \ modules/websearch/bin/webcoll \ diff --git a/modules/Makefile.am b/modules/Makefile.am index 0d72e5de03..469ab7c02b 100644 --- a/modules/Makefile.am +++ b/modules/Makefile.am @@ -52,6 +52,7 @@ SUBDIRS = bibauthorid \ webjournal \ weblinkback \ webmessage \ + webnews \ websearch \ websession \ webstat \ diff --git a/modules/miscutil/lib/upgrades/invenio_2013_02_15_webnews_new_db_tables.py b/modules/miscutil/lib/upgrades/invenio_2013_02_15_webnews_new_db_tables.py new file mode 100644 index 0000000000..bc01d22971 --- /dev/null +++ b/modules/miscutil/lib/upgrades/invenio_2013_02_15_webnews_new_db_tables.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2012 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +from invenio.dbquery import run_sql + +depends_on = ['invenio_release_1_1_0'] + +def info(): + return "New database tables for the WebNews module" + +def do_upgrade(): + query_story = \ +"""DROP TABLE IF EXISTS `nwsSTORY`; +CREATE TABLE `nwsSTORY` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(256) NOT NULL, + `body` text NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +);""" + run_sql(query_story) + + query_tag = \ +"""DROP TABLE IF EXISTS `nwsTAG`; +CREATE TABLE `nwsTAG` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `tag` varchar(64) NOT NULL, + PRIMARY KEY (`id`) +);""" + run_sql(query_tag) + + query_tooltip = \ +"""DROP TABLE IF EXISTS `nwsTOOLTIP`; +CREATE TABLE `nwsTOOLTIP` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `id_story` int(11) NOT NULL, + `body` varchar(512) NOT NULL, + `target_element` varchar(256) NOT NULL DEFAULT '', + `target_page` varchar(256) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `id_story` (`id_story`), + CONSTRAINT `nwsTOOLTIP_ibfk_1` FOREIGN KEY (`id_story`) REFERENCES `nwsSTORY` (`id`) +);""" + run_sql(query_tooltip) + + query_story_tag = \ +"""DROP TABLE IF EXISTS `nwsSTORY_nwsTAG`; +CREATE TABLE `nwsSTORY_nwsTAG` ( + `id_story` int(11) NOT NULL, + `id_tag` int(11) NOT NULL, + PRIMARY KEY (`id_story`,`id_tag`), + KEY `id_story` (`id_story`), + KEY `id_tag` (`id_tag`), + CONSTRAINT `nwsSTORY_nwsTAG_ibfk_1` FOREIGN KEY (`id_story`) REFERENCES `nwsSTORY` (`id`), + CONSTRAINT `nwsSTORY_nwsTAG_ibfk_2` FOREIGN KEY (`id_tag`) REFERENCES `nwsTAG` (`id`) +);""" + run_sql(query_story_tag) + +def estimate(): + return 1 + +def pre_upgrade(): + pass + +def post_upgrade(): + pass diff --git a/modules/webnews/Makefile.am b/modules/webnews/Makefile.am new file mode 100644 index 0000000000..b75d18399a --- /dev/null +++ b/modules/webnews/Makefile.am @@ -0,0 +1,20 @@ +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +SUBDIRS = lib web doc + +CLEANFILES = *~ diff --git a/modules/webnews/doc/Makefile.am b/modules/webnews/doc/Makefile.am new file mode 100644 index 0000000000..fb4b48b5c1 --- /dev/null +++ b/modules/webnews/doc/Makefile.am @@ -0,0 +1,18 @@ +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +CLEANFILES = *~ diff --git a/modules/webnews/lib/Makefile.am b/modules/webnews/lib/Makefile.am new file mode 100644 index 0000000000..0059e32654 --- /dev/null +++ b/modules/webnews/lib/Makefile.am @@ -0,0 +1,36 @@ +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +pylibdir = $(libdir)/python/invenio +webdir = $(localstatedir)/www/img +jsdir = $(localstatedir)/www/js + +pylib_DATA = webnews.py \ + webnews_config.py \ + webnews_webinterface.py \ + webnews_dblayer.py \ + webnews_utils.py + +js_DATA = webnews.js + +web_DATA = webnews.css + +EXTRA_DIST = $(pylib_DATA) \ + $(js_DATA) \ + $(web_DATA) + +CLEANFILES = *~ *.tmp *.pyc diff --git a/modules/webnews/lib/webnews.css b/modules/webnews/lib/webnews.css new file mode 100644 index 0000000000..b6de6eca33 --- /dev/null +++ b/modules/webnews/lib/webnews.css @@ -0,0 +1,162 @@ +/*************************************************************************** +* Assume that we want the arrow's size (i.e. side; height in case it's on * +* the left or right side; width otherwise) to be 16px. Then the border * +* width for the arrow and its border will be half that dimension, 8px. In * +* order for the arrow to be right on the tooltip, the arrow border's left * +* positioning has to be as much as it's size, therefore -16px. The arrow's * +* left positioning has to be a couple of pixels to the right in order for * +* the border effect to be visible, therefore -14px. At the same time, in * +* order for the arrow's point to not overlap with the targeted item, the * +* tooltip's left margin has to be half the arrow's size (=the arrow's * +* border width), 8px. The tooltip's padding has to at least as much as the * +* arrow's left positioning overhead, (-(-16px-(-14px))) 2px in this case. * +* The arrow's and the arrow border's top positioning can be as much as we * +* want, and it must be the same for both. * +* * +* Desired arrow's size ([height|width]): 16px * +* Arrow's border-width: 8px (half the arrow's size) * +* Tooltip's [left|right] margin: 8px (half the arrow's size) * +* Arrow's [left|right] positioning: -16px & -14px (arrow border and arrow) * +* Arrow's [top|bottom] positioning: 8px * +* * +***************************************************************************/ + +/* Arrow implementation using traditional elements */ +.ttn { + /* Display */ + position: absolute; + display: none; + z-index: 9997; + + /* Dimensions */ + max-width: 250px; + min-width: 150px; + + /* Contents */ + background: #FFFFCC; + margin-left: 8px; + padding: 5px; /* padding has to be at least 2 */ + font-size: small; + + /* Border */ + border: 1px solid #FFCC00; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + + /* Shadow */ + -moz-box-shadow: 1px 1px 3px gray; + -webkit-box-shadow: 1px 1px 3px gray; + -o-box-shadow: 1px 1px 3px gray; + box-shadow: 1px 1px 3px gray; +} + +.ttn_arrow { + /* Display */ + position: absolute; + left: -14px; + top: 8px; + z-index: 9999; + + /* Dimensions */ + height: 0; + width: 0; + + /* Border */ + border-color: transparent #FFFFCC transparent transparent; + border-color: rgba(255,255,255,0) #FFFFCC rgba(255,255,255,0) rgba(255,255,255,0); + border-style: solid; + border-width: 8px; +} + +.ttn_arrow_border { + /* Display */ + position: absolute; + left: -16px; + top: 8px; + z-index: 9998; + + /* Dimensions */ + height: 0; + width: 0; + + /* Border */ + border-color: transparent #FFCC00 transparent transparent; + border-color: rgba(255,255,255,0) #FFCC00 rgba(255,255,255,0) rgba(255,255,255,0); + border-style: solid; + border-width: 8px; +} + +/* Arrow implementation using the :before and :after pseudo elements */ +.ttn_alt_arrow_box { + /* Display */ + position: absolute; + display: none; + z-index: 9997; + + /* Dimensions */ + max-width: 250px; + min-width: 150px; + + /* Contents */ + background: #FFFFCC; + margin-left: 6px; + padding: 3px; + + /* Border */ + border: 1px solid #FFCC00; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + + /* Shadow */ + -moz-box-shadow: 1px 1px 3px gray; + -webkit-box-shadow: 1px 1px 3px gray; + -o-box-shadow: 1px 1px 3px gray; + box-shadow: 1px 1px 3px gray; +} + +.ttn_alt_arrow_box:after, .ttn_alt_arrow_box:before { + right: 100%; + border: 1px solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ttn_alt_arrow_box:after { + border-color: transparent; + border-right-color: #FFFFCC; + border-width: 6px; + top: 13px; +} + +.ttn_alt_arrow_box:before { + border-color: transparent; + border-right-color: #FFCC00; + border-width: 8px; + top: 11px; +} + +/* Style for the tooltip's contents */ +.ttn_text { + /* Contents */ + vertical-align: top; + text-align: left; +} + +.ttn_actions { + /* Contents */ + vertical-align: bottom; + text-align: right; +} + +.ttn_actions_read_more { +} + +.ttn_actions_dismiss { +} diff --git a/modules/webnews/lib/webnews.js b/modules/webnews/lib/webnews.js new file mode 100644 index 0000000000..75c953fcaa --- /dev/null +++ b/modules/webnews/lib/webnews.js @@ -0,0 +1,256 @@ +/****************************************************************************** +* WebNews JavaScript Library +* +* Includes functions to create and place the tooltips, and re-place them +* in case the browser window is resized. +* +* TODO: remove magic numbers and colors +* +******************************************************************************/ + +// Tooltips entry function. Create all the tooltips. +function create_tooltips(data) { + + // Get all the tooltips. + var tooltips = data['tooltips']; + + // Only proceed if there are tooltips. + if ( tooltips != undefined ) { + + // Get the story id and ln, to be used to create the "Read more" URL. + var story_id = data['story_id']; + var ln = data['ln']; + + // Keep an array of the tooltip notification and target elements, + // to be used to speed up the window resize function later. + var tooltips_elements = []; + + // Create each tooltip and get its notification and target elements. + for (var i = 0; i < tooltips.length; i++) { + tooltip = tooltips[i]; + tooltip_elements = create_tooltip(tooltip, story_id, ln); + tooltips_elements.push(tooltip_elements); + } + + /* + // To cover most cases, we need to call re_place_tooltip both on page + // resize and page scroll. So, let's combine both events usings ".on()" + $(window).resize(function() { + for (var i = 0; i < tooltips_elements.length; i++) { + var tooltip_notification = tooltips_elements[i][0]; + var tooltip_target = tooltips_elements[i][1]; + re_place_tooltip(tooltip_notification, tooltip_target); + } + }); + + $(window).scroll(function() { + for (var i = 0; i < tooltips_elements.length; i++) { + var tooltip_notification = tooltips_elements[i][0]; + var tooltip_target = tooltips_elements[i][1]; + re_place_tooltip(tooltip_notification, tooltip_target); + } + }); + */ + + $(window).on("resize scroll", function() { + for (var i = 0; i < tooltips_elements.length; i++) { + var tooltip_notification = tooltips_elements[i][0]; + var tooltip_target = tooltips_elements[i][1]; + re_place_tooltip(tooltip_notification, tooltip_target); + } + }); + + } + +} + +function create_tooltip(tooltip, story_id, ln) { + + // Get the tooltip data. + var id = tooltip['id']; + var target = tooltip['target']; + var body = tooltip['body']; + var readmore = tooltip['readmore']; + var dismiss = tooltip['dismiss']; + + // Create the "Read more" URL. + var readmore_url = '/news/story?id=' + story_id + '&ln=' + ln + + // Construct the tooltip html. + var tooltip_html = '
\n'; + tooltip_html += '
' + body + '
\n'; + tooltip_html += '
\n'; + // TODO: Do not add the "Read more" label until the /news interface is ready. + //tooltip_html += ' ' + readmore + '\n'; + //tooltip_html += '  | \n'; + tooltip_html += ' ' + dismiss + '\n'; + tooltip_html += '
\n'; + tooltip_html += '
\n'; + tooltip_html += '
\n'; + tooltip_html += '
\n'; + + // Append the tooltip html to the body. + $('body').append(tooltip_html); + + // Create the jquery element selectors for the tooltip notification and target. + var tooltip_notification = $("#" + id); + var tooltip_target = eval(target); + + // Place and display the tooltip. + place_tooltip(tooltip_notification, tooltip_target); + + // Return the tooltip notification and target elements in an array. + return [tooltip_notification, tooltip_target]; +} + +function place_tooltip(tooltip_notification, tooltip_target) { + + // Only display the tooltip if the tooltip_notification exists + // and the tooltip exists and is visible. + if ( tooltip_notification.length > 0 && tooltip_target.length > 0 && tooltip_target.is(":visible") ) { + + // First, calculate the top of tooltip_notification: + // This comes from tooltip_target's top with some adjustments + // in order to place the tooltip in the middle of the tooltip_target. + var tooltip_target_height = tooltip_target.outerHeight(); + var tooltip_target_top = tooltip_target.offset().top; + // The distance from the top of the tooltip_notifcation to the + // arrow's tip is 16px (half the arrow's size + arrow's top margin) + var tooltip_notification_top = tooltip_target_top + ( tooltip_target_height / 2 ) - 16 + if ( tooltip_notification_top < 0 ) { + tooltip_notification_top = 0; + } + + // Second, calculate the left of tooltip_notification: + // This comes from the sum of tooltip_target's left and width + var tooltip_target_left = tooltip_target.offset().left; + var tooltip_target_width = tooltip_target.outerWidth(); + var tooltip_notification_left = tooltip_target_left + tooltip_target_width; + + // However, if tooltip_notification appears to be displayed outside the window, + // then we have to place it on the other side of tooltip_target + var tooltip_notification_width = tooltip_notification.outerWidth(); + var window_width = $(window).width(); + if ( ( tooltip_notification_left + tooltip_notification_width ) > window_width ) { + // Place tooltip_notification on the other side, taking into account the arrow's size + // The left margin of the tooltip_notification and half the + // arrow's size is 16px + tooltip_notification_left = tooltip_target_left - tooltip_notification_width - 16; + // Why does 4px work perfectly here? + tooltip_notification.children("div[class='ttn_arrow']").css("left", (tooltip_notification_width - 4) + "px"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "transparent transparent transparent #FFFFCC"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #FFFFCC"); + // 2px is 4px - 2px here, since the arrow's border has a 2px offset from the arrow + tooltip_notification.children("div[class='ttn_arrow_border']").css("left", "").css("left", (tooltip_notification_width - 2) + "px"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "transparent transparent transparent #FFCC00"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #FFCC00"); + tooltip_notification.css("-moz-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("-webkit-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("-o-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("box-shadow", "-1px 1px 3px gray"); + } + + // Set the final attributes and display tooltip_notification + tooltip_notification.css('top', tooltip_notification_top + 'px'); + tooltip_notification.css('left', tooltip_notification_left + 'px'); + tooltip_notification.fadeIn(); + tooltip_notification.find("a[class='ttn_actions_dismiss']").click(function() { + $.ajax({ + url: "/news/dismiss", + data: { tooltip_notification_id: tooltip_notification.attr("id") }, + success: function(data) { + if ( data["success"] == 1 ) { + tooltip_notification.fadeOut(); + } + }, + dataType: "json" + }); + }); + + } + +} + +function re_place_tooltip(tooltip_notification, tooltip_target) { + + // Only display the tooltip if the tooltip_notification exists + // and the tooltip exists and is visible. + if ( tooltip_notification.length > 0 && tooltip_notification.is(":visible") && tooltip_target.length > 0 && tooltip_target.is(":visible") ) { + + // First, calculate the top of tooltip_notification: + // This comes from tooltip_target's top with some adjustments + // in order to place the tooltip in the middle of the tooltip_target. + var tooltip_target_height = tooltip_target.outerHeight(); + var tooltip_target_top = tooltip_target.offset().top; + // The distance from the top of the tooltip_notifcation to the + // arrow's tip is 16px (half the arrow's size + arrow's top margin) + var tooltip_notification_top = tooltip_target_top + ( tooltip_target_height / 2 ) - 16 + if ( tooltip_notification_top < 0 ) { + tooltip_notification_top = 0; + } + + // Second, calculate the left of tooltip_notification: + // This comes from the sum of tooltip_target's left and width + var tooltip_target_left = tooltip_target.offset().left; + var tooltip_target_width = tooltip_target.outerWidth(); + var tooltip_notification_left = tooltip_target_left + tooltip_target_width; + + // However, if tooltip_notification appears to be displayed outside the window, + // then we have to place it on the other side of tooltip_target + var tooltip_notification_width = tooltip_notification.outerWidth(); + var window_width = $(window).width(); + if ( ( tooltip_notification_left + tooltip_notification_width ) > window_width ) { + // Place tooltip_notification on the other side, taking into account the arrow's size + // The left margin of the tooltip_notification and half the + // arrow's size is 16px + tooltip_notification_left = tooltip_target_left - tooltip_notification_width - 16; + // Why does 4px work perfectly here? + tooltip_notification.children("div[class='ttn_arrow']").css("left", (tooltip_notification_width - 4) + "px"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "transparent transparent transparent #FFFFCC"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #FFFFCC"); + // 2px is 4px - 2px here, since the arrow's border has a 2px offset from the arrow + tooltip_notification.children("div[class='ttn_arrow_border']").css("left", "").css("left", (tooltip_notification_width - 2) + "px"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "transparent transparent transparent #FFCC00"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "rgba(255,255,255,0) rgba(255,255,255,0) rgba(255,255,255,0) #FFCC00"); + tooltip_notification.css("-moz-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("-webkit-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("-o-box-shadow", "-1px 1px 3px gray"); + tooltip_notification.css("box-shadow", "-1px 1px 3px gray"); + } + else { + // The original left position of the tooltip_notification's arrow is -14px + tooltip_notification.children("div[class='ttn_arrow']").css("left", "-14px"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "transparent #FFFFCC transparent transparent"); + tooltip_notification.children("div[class='ttn_arrow']").css("border-color", "rgba(255,255,255,0) #FFFFCC rgba(255,255,255,0) rgba(255,255,255,0)"); + // The original left position of the tooltip_notification's arrow border is -16px + tooltip_notification.children("div[class='ttn_arrow_border']").css("left", "").css("left", "-16px"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "transparent #FFCC00 transparent transparent"); + tooltip_notification.children("div[class='ttn_arrow_border']").css("border-color", "rgba(255,255,255,0) #FFCC00 rgba(255,255,255,0) rgba(255,255,255,0)"); + tooltip_notification.css("-moz-box-shadow", "1px 1px 3px gray"); + tooltip_notification.css("-webkit-box-shadow", "1px 1px 3px gray"); + tooltip_notification.css("-o-box-shadow", "1px 1px 3px gray"); + tooltip_notification.css("box-shadow", "1px 1px 3px gray"); + } + + // Set the final attributes for tooltip_notification + tooltip_notification.css('top', tooltip_notification_top + 'px'); + tooltip_notification.css('left', tooltip_notification_left + 'px'); + + // If the tooltip_notification was previously hidden, show it. + if ( !tooltip_notification.is(":visible") ) { + tooltip_notification.show(); + } + + } + + else { + + // If the tooltip_notification was previously visible, hide it. + if ( tooltip_notification.is(":visible") ) { + tooltip_notification.hide(); + } + + } + +} + diff --git a/modules/webnews/lib/webnews.py b/modules/webnews/lib/webnews.py new file mode 100644 index 0000000000..a1d15dfae8 --- /dev/null +++ b/modules/webnews/lib/webnews.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +""" WebNews module """ + +__revision__ = "$Id$" + +# GENERAL IMPORTS +from cgi import escape +import sys +CFG_JSON_AVAILABLE = True +if sys.hexversion < 0x2060000: + try: + import simplejson as json + except: + CFG_JSON_AVAILABLE = False +else: + import json + +# GENERAL IMPORTS +from urlparse import urlsplit +import time + +# INVENIO IMPORTS +from invenio.config import CFG_SITE_LANG +from invenio.webinterface_handler_wsgi_utils import Cookie, \ + get_cookie + # INFO: Old API, ignore + #add_cookies, \ +from invenio.messages import gettext_set_language +from invenio.urlutils import get_referer + +# MODULE IMPORTS +from invenio.webnews_dblayer import get_latest_story_id, \ + get_story_tooltips +from invenio.webnews_config import CFG_WEBNEWS_TOOLTIPS_DISPLAY, \ + CFG_WEBNEWS_TOOLTIPS_COOKIE_LONGEVITY, \ + CFG_WEBNEWS_TOOLTIPS_COOKIE_NAME + +def _create_tooltip_cookie(name = CFG_WEBNEWS_TOOLTIPS_COOKIE_NAME, + value = "", + path = "/", + longevity = CFG_WEBNEWS_TOOLTIPS_COOKIE_LONGEVITY): + """ + Private shortcut function that returns an instance of a Cookie for the + tooltips. + """ + + # The local has to be English for this to work! + longevity_time = time.time() + ( longevity * 24 * 60 * 60 ) + longevity_expression = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(longevity_time)) + + cookie = Cookie(name, + value, + path = path, + expires = longevity_expression) + + return cookie + +def _paths_match(path1, path2): + """ + Internal path matcher. + "*" acts as a wildcard for individual path parts. + """ + + # Start off by assuming that the paths don't match + paths_match_p = False + + # Get the individual path parts. + path1_parts = path1.strip("/").split("/") + path2_parts = path2.strip("/").split("/") + + # If the 2 paths have different number of parts don't even bother checking + if len(path1_parts) == len(path2_parts): + # Check if the individual path parts match + for (part1, part2) in zip(path1_parts, path2_parts): + paths_match_p = ( part1 == part2 ) or ( "*" in (part1, part2) ) + if not paths_match_p: + break + + return paths_match_p + +def _does_referer_match_target_page(referer, + target_page): + """ + Compares the referer and the target page in a smart way and + returns True if they match, otherwise False. + """ + + try: + return ( target_page == "*" ) or _paths_match(urlsplit(target_page)[2], urlsplit(referer)[2]) + except: + return False + +def perform_request_tooltips(req = None, + uid = 0, + story_id = 0, + tooltip_id = 0, + ln = CFG_SITE_LANG): + """ + Calculates and returns the tooltips information in JSON. + """ + + tooltips_dict = {} + + #tooltips_json = json.dumps(tooltips_dict) + tooltips_json = '{}' + + # Did we import json? + # Should we display tooltips at all? + # Does the request exist? + if not CFG_JSON_AVAILABLE or not CFG_WEBNEWS_TOOLTIPS_DISPLAY or req is None: + return tooltips_json + + if story_id == 0: + # Is there a latest story to display? + story_id = get_latest_story_id() + + if story_id is None: + return tooltips_json + else: + # Are there any tooltips associated to this story? + # TODO: Filter the unwanted tooltips in the DB query. + # We can already filter by REFERER and by the tooltips IDs in the cookie + # In that case we don't have to iterate through the tooltips later and + # figure out which ones to keep. + tooltips = get_story_tooltips(story_id) + if tooltips is None: + return tooltips_json + + # In a more advance scenario we would save the information on whether the + # the user has seen the tooltips: + # * in a session param for the users that have logged in + # * in a cookie for guests + # We could then use a combination of these two to decide whether to display + # the tooltips or not. + # + # In that case, the following tools could be used: + #from invenio.webuser import isGuestUser, \ + # session_param_get, \ + # session_param_set + #is_user_guest = isGuestUser(uid) + #if not is_user_guest: + # try: + # tooltip_information = session_param_get(req, CFG_WEBNEWS_TOOLTIPS_SESSION_PARAM_NAME) + # except KeyError: + # session_param_set(req, CFG_WEBNEWS_TOOLTIPS_SESSION_PARAM_NAME, "") + + cookie_name = "%s_%s" % (CFG_WEBNEWS_TOOLTIPS_COOKIE_NAME, str(story_id)) + + try: + # Get the cookie + cookie = get_cookie(req, cookie_name) + # Get the tooltip IDs that have already been displayed + tooltips_in_cookie = filter(None, str(cookie.value).split(",")) + except: + # TODO: Maybe set a cookie with an emptry string as value? + tooltips_in_cookie = [] + + # Prepare the user's prefered language and labels. + _ = gettext_set_language(ln) + readmore_label = _("Learn more") + dismiss_label = _("I got it!") + + # Get the referer, in order to check if we should display + # the tooltip in the given page. + referer = get_referer(req) + + tooltips_list = [] + + for tooltip in tooltips: + tooltip_notification_id = 'ttn_%s_%s' % (str(story_id), str(tooltip[0])) + # INFO: the tooltip body is not escaped! + # it's up to the admin to insert proper body text. + #tooltip_body = escape(tooltip[1], True) + tooltip_body = tooltip[1] + tooltip_target_element = tooltip[2] + tooltip_target_page = tooltip[3] + + # Only display the tooltips that match the referer and that the user + # has not already seen. + if _does_referer_match_target_page(referer, tooltip_target_page) and \ + ( tooltip_notification_id not in tooltips_in_cookie ): + + # Add this tooltip to the tooltips that we will display. + tooltips_list.append({ + 'id' : tooltip_notification_id, + 'target' : tooltip_target_element, + 'body' : tooltip_body, + 'readmore' : readmore_label, + 'dismiss' : dismiss_label, + }) + + # Add this tooltip to the tooltips that the user has already seen. + #tooltips_in_cookie.append(tooltip_notification_id) + + if tooltips_list: + # Hooray! There are some tooltips to display! + tooltips_dict['tooltips'] = tooltips_list + tooltips_dict['story_id'] = str(story_id) + tooltips_dict['ln'] = ln + + # Create and set the updated cookie. + #cookie_value = ",".join(tooltips_in_cookie) + #cookie = _create_tooltip_cookie(cookie_name, + # cookie_value) + #req.set_cookie(cookie) + ## INFO: Old API, ignore + ##add_cookies(req, [cookie]) + + # JSON-ify and return the tooltips. + tooltips_json = json.dumps(tooltips_dict) + return tooltips_json + +def perform_request_dismiss(req = None, + uid = 0, + story_id = 0, + tooltip_notification_id = None): + """ + Dismisses the given tooltip for the current user. + """ + + try: + + if not CFG_JSON_AVAILABLE or not CFG_WEBNEWS_TOOLTIPS_DISPLAY or req is None: + raise Exception("Tooltips are not currently available.") + + # Retrieve the story_id + if story_id == 0: + if tooltip_notification_id is None: + raise Exception("No tooltip_notification_id has been given.") + else: + story_id = tooltip_notification_id.split("_")[1] + + # Generate the cookie name out of the story_id + cookie_name = "%s_%s" % (CFG_WEBNEWS_TOOLTIPS_COOKIE_NAME, str(story_id)) + + # Get the existing tooltip_notification_ids from the cookie + try: + # Get the cookie + cookie = get_cookie(req, cookie_name) + # Get the tooltip IDs that have already been displayed + tooltips_in_cookie = filter(None, str(cookie.value).split(",")) + except: + # TODO: Maybe set a cookie with an emptry string as value? + tooltips_in_cookie = [] + + # Append the tooltip_notification_id to the existing tooltip_notification_ids + # (only if it's not there already ; but normally it shouldn't be) + if tooltip_notification_id not in tooltips_in_cookie: + tooltips_in_cookie.append(tooltip_notification_id) + + # Create and set the cookie with the updated cookie value + cookie_value = ",".join(tooltips_in_cookie) + cookie = _create_tooltip_cookie(cookie_name, cookie_value) + req.set_cookie(cookie) + # INFO: Old API, ignore + #add_cookies(req, [cookie]) + + except: + # Something went wrong.. + # TODO: what went wrong? + dismissed_p_dict = { "success" : 0 } + dismissed_p_json = json.dumps(dismissed_p_dict) + return dismissed_p_json + else: + # Everything went great! + dismissed_p_dict = { "success" : 1 } + dismissed_p_json = json.dumps(dismissed_p_dict) + return dismissed_p_json + # enable for python >= 2.5 + #finally: + # # JSON-ify and return the result + # dismissed_p_json = json.dumps(dismissed_p_dict) + # return dismissed_p_json diff --git a/modules/webnews/lib/webnews_config.py b/modules/webnews/lib/webnews_config.py new file mode 100644 index 0000000000..c3154176f0 --- /dev/null +++ b/modules/webnews/lib/webnews_config.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +""" WebNews module configuration """ + +__revision__ = "$Id$" + +# Should we generally display tooltips or not? +CFG_WEBNEWS_TOOLTIPS_DISPLAY = True + +# The tooltips session param name +#CFG_WEBNEWS_TOOLTIPS_SESSION_PARAM_NAME = "has_user_seen_tooltips" + +# Tooltips cookie settings +# The cookie name +CFG_WEBNEWS_TOOLTIPS_COOKIE_NAME = "INVENIOTOOLTIPS" +# the cookie longevity in days +CFG_WEBNEWS_TOOLTIPS_COOKIE_LONGEVITY = 14 diff --git a/modules/webnews/lib/webnews_dblayer.py b/modules/webnews/lib/webnews_dblayer.py new file mode 100644 index 0000000000..c013377b95 --- /dev/null +++ b/modules/webnews/lib/webnews_dblayer.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +""" Database related functions for the WebNews module """ + +__revision__ = "$Id$" + +# INVENIO IMPORTS +from invenio.dbquery import run_sql + +# MODULE IMPORTS +from invenio.webnews_utils import convert_xpath_expression_to_jquery_selector +from invenio.webnews_config import CFG_WEBNEWS_TOOLTIPS_COOKIE_LONGEVITY + +def get_latest_story_id(): + """ + Returns the id of the latest news story available. + """ + + query = """ SELECT id + FROM nwsSTORY + WHERE created >= DATE_SUB(CURDATE(),INTERVAL %s DAY) + ORDER BY created DESC + LIMIT 1""" + + params = (CFG_WEBNEWS_TOOLTIPS_COOKIE_LONGEVITY,) + + res = run_sql(query, params) + + if res: + return res[0][0] + + return None + +def get_story_tooltips(story_id): + """ + Returns all the available tooltips for the given story ID. + """ + + query = """ SELECT id, + body, + target_element, + target_page + FROM nwsTOOLTIP + WHERE id_story=%s""" + + params = (story_id,) + + res = run_sql(query, params) + + if res: + return res + return None + +def update_tooltip(story_id, + tooltip_id, + tooltip_body, + tooltip_target_element, + tooltip_target_page, + is_tooltip_target_xpath = False): + """ + Updates the tooltip information. + XPath expressions are automatically translated to the equivalent jQuery + selector if so chosen by the user. + """ + + query = """ UPDATE nwsTOOLTIP + SET body=%s, + target_element=%s, + target_page=%s + WHERE id=%s + AND id_story=%s""" + + tooltip_target_element = is_tooltip_target_xpath and \ + convert_xpath_expression_to_jquery_selector(tooltip_target_element) or \ + tooltip_target_element + + params = (tooltip_body, tooltip_target_element, tooltip_target_page, tooltip_id, story_id) + + res = run_sql(query, params) + + return res + diff --git a/modules/webnews/lib/webnews_utils.py b/modules/webnews/lib/webnews_utils.py new file mode 100644 index 0000000000..36c0bea811 --- /dev/null +++ b/modules/webnews/lib/webnews_utils.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +""" Utlis for the WebNews module """ + +__revision__ = "$Id$" + +# GENERAL IMPORTS +import re + +# CONSTANT VARIABLES +# Regex for the nth element +PAT_NTH = r'(\w+)\[(\d+)\]' +def REP_NTH(matchobj): + return "%s:eq(%s)" % (str(matchobj.group(1)), str(int(matchobj.group(2))-1)) +FLG_NTH = 0 +# Regex for the id attribute +PAT_IDA = r'\*\[@id=[\'"]([\w\-]+)[\'"]\]' +REP_IDA = r'#\1' +FLG_IDA = 0 + +def convert_xpath_expression_to_jquery_selector(xpath_expression): + """ + Given an XPath expression this function + returns the equivalent jQuery selector. + """ + + tmp_result = xpath_expression.strip('/') + tmp_result = tmp_result.split('/') + tmp_result = [_x2j(e) for e in zip(tmp_result, range(len(tmp_result)))] + jquery_selector = '.'.join(tmp_result) + + return jquery_selector + +def _x2j((s, i)): + """ + Private helper function that converts each element of an XPath expression + to the equivalent jQuery selector using regular expressions. + """ + + s = re.sub(PAT_IDA, REP_IDA, s, FLG_IDA) + s = re.sub(PAT_NTH, REP_NTH, s, FLG_NTH) + s = '%s("%s")' % (i and 'children' or '$', s) + + return s + diff --git a/modules/webnews/lib/webnews_webinterface.py b/modules/webnews/lib/webnews_webinterface.py new file mode 100644 index 0000000000..2af61fedc8 --- /dev/null +++ b/modules/webnews/lib/webnews_webinterface.py @@ -0,0 +1,81 @@ +## This file is part of Invenio. +## Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +"""WebNews Web Interface.""" + +__revision__ = "$Id$" + +__lastupdated__ = """$Date$""" + +# INVENIO IMPORTS +from invenio.webinterface_handler import wash_urlargd, WebInterfaceDirectory +from invenio.config import CFG_ACCESS_CONTROL_LEVEL_SITE, \ + CFG_SITE_LANG +from invenio.webuser import getUid, page_not_authorized + +# MODULE IMPORTS +from invenio.webnews import perform_request_tooltips, \ + perform_request_dismiss + +class WebInterfaceWebNewsPages(WebInterfaceDirectory): + """ + Defines the set of /news pages. + """ + + _exports = ["tooltips", "dismiss"] + + def tooltips(self, req, form): + """ + Returns the news tooltips information in JSON. + """ + + argd = wash_urlargd(form, {'story_id' : (int, 0), + 'tooltip_id' : (int, 0), + 'ln' : (str, CFG_SITE_LANG)}) + + uid = getUid(req) + if uid == -1 or CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "../news/tooltip", + navmenuid = 'news') + + tooltips_json = perform_request_tooltips(req = req, + uid=uid, + story_id=argd['story_id'], + tooltip_id=argd['tooltip_id'], + ln=argd['ln']) + + return tooltips_json + + def dismiss(self, req, form): + """ + Dismiss the given tooltip for the current user. + """ + + argd = wash_urlargd(form, {'story_id' : (int, 0), + 'tooltip_notification_id' : (str, None)}) + + uid = getUid(req) + if uid == -1 or CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "../news/dismiss", + navmenuid = 'news') + + dismissed_p_json = perform_request_dismiss(req = req, + uid=uid, + story_id=argd['story_id'], + tooltip_notification_id=argd['tooltip_notification_id']) + + return dismissed_p_json diff --git a/modules/webnews/web/Makefile.am b/modules/webnews/web/Makefile.am new file mode 100644 index 0000000000..a6a1d24e56 --- /dev/null +++ b/modules/webnews/web/Makefile.am @@ -0,0 +1,18 @@ +## This file is part of Invenio. +## Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. +## +## Invenio 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. +## +## Invenio 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 Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +CLEANFILES = *~ *.tmp diff --git a/modules/webstyle/lib/webinterface_layout.py b/modules/webstyle/lib/webinterface_layout.py index 494dbed8d6..7534523114 100644 --- a/modules/webstyle/lib/webinterface_layout.py +++ b/modules/webstyle/lib/webinterface_layout.py @@ -301,6 +301,12 @@ def _lookup(self, component, path): register_exception(alert_admin=True, subject='EMERGENCY') WebInterfaceAuthorlistPages = WebInterfaceDumbPages +try: + from invenio.webnews_webinterface import WebInterfaceWebNewsPages +except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceWebNewsPages = WebInterfaceDumbPages + if CFG_OPENAIRE_SITE: try: from invenio.openaire_deposit_webinterface import \ @@ -369,6 +375,7 @@ class WebInterfaceInvenio(WebInterfaceSearchInterfacePages): 'goto', 'info', 'authorlist', + 'news', ] + test_exports + openaire_exports def __init__(self): @@ -410,6 +417,7 @@ def __init__(self): yourcomments = WebInterfaceDisabledPages() goto = WebInterfaceDisabledPages() authorlist = WebInterfaceDisabledPages() + news = WebInterfaceDisabledPages() else: submit = WebInterfaceSubmitPages() youraccount = WebInterfaceYourAccountPages() @@ -442,7 +450,7 @@ def __init__(self): yourcomments = WebInterfaceYourCommentsPages() goto = WebInterfaceGotoPages() authorlist = WebInterfaceAuthorlistPages() - + news = WebInterfaceWebNewsPages() # This creates the 'handler' function, which will be invoked directly # by mod_python. diff --git a/modules/webstyle/lib/webstyle_templates.py b/modules/webstyle/lib/webstyle_templates.py index 316620ebaa..435de02d68 100644 --- a/modules/webstyle/lib/webstyle_templates.py +++ b/modules/webstyle/lib/webstyle_templates.py @@ -42,6 +42,8 @@ CFG_INSPIRE_SITE, \ CFG_WEBLINKBACK_TRACKBACK_ENABLED +from invenio.webnews_config import CFG_WEBNEWS_TOOLTIPS_DISPLAY + from invenio.messages import gettext_set_language, language_list_long, is_language_rtl from invenio.urlutils import make_canonical_urlargd, create_html_link, \ get_canonical_and_alternates_urls @@ -389,6 +391,10 @@ def tmpl_pageheader(self, req, ln=CFG_SITE_LANG, headertitle="", %(hepDataAdditions)s + + + + %(metaheaderadd)s @@ -546,6 +552,25 @@ def tmpl_pagefooter(self, req=None, ln=CFG_SITE_LANG, lastupdated=None, else: msg_lastupdated = "" + if CFG_WEBNEWS_TOOLTIPS_DISPLAY: + tooltips_script = """ + +""" + else: + tooltips_script = "" + out = """ +%(tooltips_script)s """ % { @@ -588,6 +614,8 @@ def tmpl_pagefooter(self, req=None, ln=CFG_SITE_LANG, lastupdated=None, 'version': CFG_VERSION, 'pagefooteradd': pagefooteradd, + + 'tooltips_script' : tooltips_script, } return out