From 2e9bbd5dedb92bda4361db2fed4f19158d522e1e Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Sat, 17 Aug 2013 04:53:39 +0200 Subject: [PATCH 01/83] WebLinkback: faster email notification * Improves database layer to send email notification faster. * Extends regression tests for this improvement. * Applies minor clean-up. --- modules/weblinkback/lib/weblinkback.py | 15 ++++++---- .../weblinkback/lib/weblinkback_dblayer.py | 30 +++++++++++++++---- .../lib/weblinkback_regression_tests.py | 10 +++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/modules/weblinkback/lib/weblinkback.py b/modules/weblinkback/lib/weblinkback.py index c8bb9a091c..302da0ac94 100644 --- a/modules/weblinkback/lib/weblinkback.py +++ b/modules/weblinkback/lib/weblinkback.py @@ -157,20 +157,25 @@ def send_pending_linkbacks_notification(linkback_type): Send notification emails to all linkback moderators for all pending linkbacks @param linkback_type: of CFG_WEBLINKBACK_LIST_TYPE """ - pending_linkbacks = get_all_linkbacks(linkback_type=CFG_WEBLINKBACK_TYPE['TRACKBACK'], status=CFG_WEBLINKBACK_STATUS['PENDING']) + pending_linkbacks = get_all_linkbacks(linkback_type=CFG_WEBLINKBACK_TYPE['TRACKBACK'], + status=CFG_WEBLINKBACK_STATUS['PENDING'], + limit=CFG_WEBLINKBACK_MAX_LINKBACKS_IN_EMAIL) if pending_linkbacks: - pending_count = len(pending_linkbacks) + pending_count = get_all_linkbacks(linkback_type=CFG_WEBLINKBACK_TYPE['TRACKBACK'], + status=CFG_WEBLINKBACK_STATUS['PENDING'], + full_count_only=True) + cutoff_text = '' if pending_count > CFG_WEBLINKBACK_MAX_LINKBACKS_IN_EMAIL: - cutoff_text = ' (Printing only the first %s requests)' % CFG_WEBLINKBACK_MAX_LINKBACKS_IN_EMAIL + cutoff_text = '(Printing only the first %s requests)' % CFG_WEBLINKBACK_MAX_LINKBACKS_IN_EMAIL - content = """There are %(count)s new %(linkback_type)s requests which you should approve or reject%(cutoff)s: + content = """There are %(count)s new %(linkback_type)s requests which you should approve or reject %(cutoff)s: """ % {'count': pending_count, 'linkback_type': linkback_type, 'cutoff': cutoff_text} - for pending_linkback in pending_linkbacks[0:CFG_WEBLINKBACK_MAX_LINKBACKS_IN_EMAIL]: + for pending_linkback in pending_linkbacks: content += """ For %(recordURL)s from %(origin_url)s. """ % {'recordURL': generate_redirect_url(pending_linkback[2]), diff --git a/modules/weblinkback/lib/weblinkback_dblayer.py b/modules/weblinkback/lib/weblinkback_dblayer.py index f11761cc62..ebbc89ff8e 100644 --- a/modules/weblinkback/lib/weblinkback_dblayer.py +++ b/modules/weblinkback/lib/weblinkback_dblayer.py @@ -27,13 +27,20 @@ from invenio.textutils import xml_entities_to_utf8 -def get_all_linkbacks(recid=None, status=None, order=CFG_WEBLINKBACK_ORDER_BY_INSERTION_TIME["ASC"], linkback_type=None): +def get_all_linkbacks(recid=None, + status=None, + order=CFG_WEBLINKBACK_ORDER_BY_INSERTION_TIME["ASC"], + linkback_type=None, + limit=None, + full_count_only=False): """ Get all linkbacks @param recid: of one record, of all if None @param status: with a certain status, of all if None @param order: order by insertion time either "ASC" or "DESC" @param linkback_type: of a certain type, of all if None + @param limit: maximum result count, all if None + @param full_count_only: return only full result count (does not consider "limit"), result set if False @return [(linkback_id, origin_url, recid, @@ -41,7 +48,9 @@ def get_all_linkbacks(recid=None, status=None, order=CFG_WEBLINKBACK_ORDER_BY_IN linkback_type, linkback_status, insert_time)] - in order by id + in order by id, up to "limited" results + + OR integer if count_only """ header_sql = """SELECT id, @@ -52,9 +61,10 @@ def get_all_linkbacks(recid=None, status=None, order=CFG_WEBLINKBACK_ORDER_BY_IN status, insert_time FROM lnkENTRY""" - conditions = [] - order_sql = "ORDER by id %s" % order + if full_count_only: + header_sql = 'SELECT count(id) FROM lnkENTRY' + conditions = [] params = [] def add_condition(column, value): @@ -69,7 +79,17 @@ def add_condition(column, value): add_condition('status', status) add_condition('type', linkback_type) - return run_sql(header_sql + ' ' + ' '.join(conditions) + ' ' + order_sql, tuple(params)) + order_sql = 'ORDER by id %s' % order + + limit_sql = '' + if limit: + limit_sql = 'LIMIT %s' % limit + + res = run_sql('%s %s %s %s' % (header_sql, ' '.join(conditions), order_sql, limit_sql), tuple(params)) + if full_count_only: + return int(res[0][0]) + else: + return res def approve_linkback(linkbackid, user_info): diff --git a/modules/weblinkback/lib/weblinkback_regression_tests.py b/modules/weblinkback/lib/weblinkback_regression_tests.py index ab1b11b38e..7f4291cb3e 100644 --- a/modules/weblinkback/lib/weblinkback_regression_tests.py +++ b/modules/weblinkback/lib/weblinkback_regression_tests.py @@ -224,6 +224,16 @@ def test_get_all_linkbacks2(self): """weblinkback - get all linkbacks with a certain status""" self.assertEqual(9, len(get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['PENDING']))) self.assertEqual(0, len(get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['APPROVED']))) + self.assertEqual(9, get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['PENDING'], full_count_only=True)) + self.assertEqual(0, get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['APPROVED'], full_count_only=True)) + + def test_get_limited_count_of_linkbacks(self): + """weblinkback - get limited count of linkbacks""" + self.assertEqual(5, len(get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['PENDING'], limit=5))) + self.assertEqual(2, len(get_all_linkbacks(recid=42, status=CFG_WEBLINKBACK_STATUS['PENDING'], limit=2))) + # Ignores limit + self.assertEqual(9, get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['PENDING'], limit=5, full_count_only=True)) + self.assertEqual(5, get_all_linkbacks(recid=42, status=CFG_WEBLINKBACK_STATUS['PENDING'], limit=2, full_count_only=True)) def test_approve_linkback(self): """weblinkback - approve linkback""" From 974f2acb8bf511e476890016e8413742248a1c1c Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Sun, 18 Aug 2013 19:01:11 +0200 Subject: [PATCH 02/83] WebLinkback: faster deletion of linkbacks * Avoids redundant creations of blacklist. --- modules/weblinkback/lib/weblinkback.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/weblinkback/lib/weblinkback.py b/modules/weblinkback/lib/weblinkback.py index 302da0ac94..4f8b1eb005 100644 --- a/modules/weblinkback/lib/weblinkback.py +++ b/modules/weblinkback/lib/weblinkback.py @@ -185,15 +185,14 @@ def send_pending_linkbacks_notification(linkback_type): send_email(CFG_SITE_ADMIN_EMAIL, email, 'Pending ' + linkback_type + ' requests', content) -def infix_exists_for_url_in_list(url, list_type): +def infix_exists_for_url_in_list(url, url_list): """ Check if an infix of a url exists in a list @param url - @param list_type, of CFG_WEBLINKBACK_LIST_TYPE + @param url_list @return True, False """ - urls = get_url_list(list_type) - for current_url in urls: + for current_url in url_list: if current_url in url: return True return False @@ -262,8 +261,8 @@ def perform_sendtrackback(req, recid, url, title, excerpt, blog_name, blog_id, s %s """ - blacklist_match = infix_exists_for_url_in_list(url, CFG_WEBLINKBACK_LIST_TYPE['BLACKLIST']) - whitelist_match = infix_exists_for_url_in_list(url, CFG_WEBLINKBACK_LIST_TYPE['WHITELIST']) + blacklist_match = infix_exists_for_url_in_list(url, get_url_list(CFG_WEBLINKBACK_LIST_TYPE['BLACKLIST'])) + whitelist_match = infix_exists_for_url_in_list(url, get_url_list(CFG_WEBLINKBACK_LIST_TYPE['WHITELIST'])) # faulty request, url argument not set if url in (CFG_WEBLINKBACK_SUBSCRIPTION_DEFAULT_ARGUMENT_NAME, None, ''): @@ -341,6 +340,7 @@ def delete_linkbacks_on_blacklist(): linkbacks.extend(list(get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['REJECTED']))) linkbacks.extend(list(get_all_linkbacks(status=CFG_WEBLINKBACK_STATUS['BROKEN']))) + blacklist = get_url_list(CFG_WEBLINKBACK_LIST_TYPE['BLACKLIST']) for linkback in linkbacks: - if infix_exists_for_url_in_list(linkback[1], CFG_WEBLINKBACK_LIST_TYPE['BLACKLIST']): + if infix_exists_for_url_in_list(linkback[1], blacklist): remove_linkback(linkback[0]) From 8beff8c9f2728dfa4b499d1c5067cbf3239ae968 Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Mon, 23 Sep 2013 20:31:32 +0200 Subject: [PATCH 03/83] SolrUtils: reliable regression test suite * Makes test suite independent from PDF extraction process. (closes #1590) * Simplifies test suite. Signed-off-by: Patrick Glauner --- .../miscutil/lib/solrutils_bibrank_indexer.py | 3 + .../lib/solrutils_regression_tests.py | 292 ++++++------------ 2 files changed, 95 insertions(+), 200 deletions(-) diff --git a/modules/miscutil/lib/solrutils_bibrank_indexer.py b/modules/miscutil/lib/solrutils_bibrank_indexer.py index fd0f83da19..e54e41e7c2 100644 --- a/modules/miscutil/lib/solrutils_bibrank_indexer.py +++ b/modules/miscutil/lib/solrutils_bibrank_indexer.py @@ -35,6 +35,9 @@ from invenio.bibrank_bridge_utils import get_tags, get_field_content_in_utf8 +SOLR_CONNECTION = None + + if CFG_SOLR_URL: import solr SOLR_CONNECTION = solr.SolrConnection(CFG_SOLR_URL) # pylint: disable=E1101 diff --git a/modules/miscutil/lib/solrutils_regression_tests.py b/modules/miscutil/lib/solrutils_regression_tests.py index 5dddd58d3e..fe471985f4 100644 --- a/modules/miscutil/lib/solrutils_regression_tests.py +++ b/modules/miscutil/lib/solrutils_regression_tests.py @@ -24,30 +24,50 @@ from invenio import intbitset from invenio.solrutils_bibindex_searcher import solr_get_bitset from invenio.solrutils_bibrank_searcher import solr_get_ranked, solr_get_similar_ranked +from invenio.solrutils_bibrank_indexer import solr_add, SOLR_CONNECTION from invenio.search_engine import get_collection_reclist from invenio.bibrank_bridge_utils import get_external_word_similarity_ranker, \ get_logical_fields, \ get_tags, \ get_field_content_in_utf8 + ROWS = 100 HITSETS = { 'Willnotfind': intbitset.intbitset([]), - 'higgs': intbitset.intbitset([47, 48, 51, 52, 55, 56, 58, 68, 79, 85, 89, 96]), - 'of': intbitset.intbitset([8, 10, 11, 12, 15, 43, 44, 45, 46, 47, 48, 49, 50, 51, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 64, 68, 74, - 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, - 91, 92, 93, 94, 95, 96, 97]), - '"higgs boson"': intbitset.intbitset([55, 56]), + 'of': intbitset.intbitset([1, 2, 7, 8, 10]), + '"of the"': intbitset.intbitset([1, 2, 7, 8, 10]) } -def get_topN(n, data): - res = dict() - for key, value in data.iteritems(): - res[key] = value[-n:] - return res + +RECORDS = xrange(1, 11) + + +TAGS = {'abstract': ['520__%'], + 'author': ['100__a', '700__a'], + 'keyword': ['6531_a'], + 'title': ['245__%', '246__%']} + + +def init_Solr(): + _delete_all() + _index_records() + SOLR_CONNECTION.commit() + + +def _delete_all(): + SOLR_CONNECTION.delete_query('*:*') + + +def _index_records(): + for recid in RECORDS: + fulltext = abstract = get_field_content_in_utf8(recid, 'abstract', TAGS) + author = get_field_content_in_utf8(recid, 'author', TAGS) + keyword = get_field_content_in_utf8(recid, 'keyword', TAGS) + title = get_field_content_in_utf8(recid, 'title', TAGS) + solr_add(recid, abstract, author, fulltext, keyword, title) class TestSolrSearch(InvenioTestCase): @@ -55,23 +75,19 @@ class TestSolrSearch(InvenioTestCase): make install-solrutils CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column - AND EITHER - Solr index built: ./bibindex -w fulltext for all records - OR - WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - and ./bibrank -w wrd for all records """ - - def _get_result(self, query, index='fulltext'): + def get_result(self, query, index='fulltext'): return solr_get_bitset(index, query) + def setUp(self): + init_Solr() + @nottest def test_get_bitset(self): """solrutils - search results""" - self.assertEqual(HITSETS['Willnotfind'], self._get_result('Willnotfind')) - self.assertEqual(HITSETS['higgs'], self._get_result('higgs')) - self.assertEqual(HITSETS['of'], self._get_result('of')) - self.assertEqual(HITSETS['"higgs boson"'], self._get_result('"higgs boson"')) + self.assertEqual(self.get_result('Willnotfind'), HITSETS['Willnotfind']) + self.assertEqual(self.get_result('of'), HITSETS['of']) + self.assertEqual(self.get_result('"of the"'), HITSETS['"of the"']) class TestSolrRanking(InvenioTestCase): @@ -79,91 +95,25 @@ class TestSolrRanking(InvenioTestCase): make install-solrutils CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column - AND EITHER - Solr index built: ./bibindex -w fulltext for all records - OR - WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - and ./bibrank -w wrd for all records """ - - def _get_ranked_result_sequence(self, query, index='fulltext', rows=ROWS, hitset=None): - if hitset is None: - hitset=HITSETS[query] - ranked_result = solr_get_ranked('%s:%s' % (index, query), hitset, self._get_ranking_params(), rows) + def get_ranked_result_sequence(self, query, index='fulltext', rows=ROWS): + ranked_result = solr_get_ranked('%s:%s' % (index, query), + HITSETS[query], + {'cutoff_amount': 10000, + 'cutoff_time_ms': 2000 + }, + rows) return tuple([pair[0] for pair in ranked_result[0]]) - def _get_ranked_topN(self, n): - return get_topN(n, self._RANKED) - - _RANKED = { - 'Willnotfind': tuple(), - 'higgs': (79, 51, 55, 47, 56, 96, 58, 68, 52, 48, 89, 85), - 'of': (50, 61, 60, 54, 56, 53, 10, 68, 44, 57, 83, 95, 92, 91, 74, 45, 48, 62, 82, - 49, 51, 89, 90, 96, 43, 8, 64, 97, 15, 85, 78, 46, 55, 79, 84, 88, 81, 52, - 58, 86, 11, 80, 93, 77, 12, 59, 87, 47, 94), - '"higgs boson"': (55, 56), - } - - def _get_ranking_params(self, cutoff_amount=10000, cutoff_time=2000): - """ - Default values from template_word_similarity_solr.cfg - """ - return { - 'cutoff_amount': cutoff_amount, - 'cutoff_time_ms': cutoff_time - } + def setUp(self): + init_Solr() @nottest def test_get_ranked(self): """solrutils - ranking results""" - all_ranked = 0 - ranked_top = self._get_ranked_topN(all_ranked) - self.assertEqual(ranked_top['Willnotfind'], self._get_ranked_result_sequence(query='Willnotfind')) - self.assertEqual(ranked_top['higgs'], self._get_ranked_result_sequence(query='higgs')) - self.assertEqual(ranked_top['of'], self._get_ranked_result_sequence(query='of')) - self.assertEqual(ranked_top['"higgs boson"'], self._get_ranked_result_sequence(query='"higgs boson"')) - - @nottest - def test_get_ranked_top(self): - """solrutils - ranking top results""" - top_n = 0 - self.assertEqual(tuple(), self._get_ranked_result_sequence(query='Willnotfind', rows=top_n)) - self.assertEqual(tuple(), self._get_ranked_result_sequence(query='higgs', rows=top_n)) - self.assertEqual(tuple(), self._get_ranked_result_sequence(query='of', rows=top_n)) - self.assertEqual(tuple(), self._get_ranked_result_sequence(query='"higgs boson"', rows=top_n)) - - top_n = 2 - ranked_top = self._get_ranked_topN(top_n) - self.assertEqual(ranked_top['Willnotfind'], self._get_ranked_result_sequence(query='Willnotfind', rows=top_n)) - self.assertEqual(ranked_top['higgs'], self._get_ranked_result_sequence(query='higgs', rows=top_n)) - self.assertEqual(ranked_top['of'], self._get_ranked_result_sequence(query='of', rows=top_n)) - self.assertEqual(ranked_top['"higgs boson"'], self._get_ranked_result_sequence(query='"higgs boson"', rows=top_n)) - - top_n = 10 - ranked_top = self._get_ranked_topN(top_n) - self.assertEqual(ranked_top['Willnotfind'], self._get_ranked_result_sequence(query='Willnotfind', rows=top_n)) - self.assertEqual(ranked_top['higgs'], self._get_ranked_result_sequence(query='higgs', rows=top_n)) - self.assertEqual(ranked_top['of'], self._get_ranked_result_sequence(query='of', rows=top_n)) - self.assertEqual(ranked_top['"higgs boson"'], self._get_ranked_result_sequence(query='"higgs boson"', rows=top_n)) - - @nottest - def test_get_ranked_smaller_hitset(self): - """solrutils - ranking smaller hitset""" - hitset = intbitset.intbitset([47, 56, 58, 68, 85, 89]) - self.assertEqual((47, 56, 58, 68, 89, 85), self._get_ranked_result_sequence(query='higgs', hitset=hitset)) - - hitset = intbitset.intbitset([45, 50, 61, 74, 94]) - self.assertEqual((50, 61, 74, 45, 94), self._get_ranked_result_sequence(query='of', hitset=hitset)) - self.assertEqual((74, 45, 94), self._get_ranked_result_sequence(query='of', hitset=hitset, rows=3)) - - @nottest - def test_get_ranked_larger_hitset(self): - """solrutils - ranking larger hitset""" - hitset = intbitset.intbitset([47, 56, 58, 68, 85, 89]) - self.assertEqual(tuple(), self._get_ranked_result_sequence(query='Willnotfind', hitset=hitset)) - - hitset = intbitset.intbitset([47, 56, 55, 56, 58, 68, 85, 89]) - self.assertEqual((55, 56), self._get_ranked_result_sequence(query='"higgs boson"', hitset=hitset)) + self.assertEqual(self.get_ranked_result_sequence(query='Willnotfind'), tuple()) + self.assertEqual(self.get_ranked_result_sequence(query='of'), (8, 2, 1, 10, 7)) + self.assertEqual(self.get_ranked_result_sequence(query='"of the"'), (8, 10, 1, 2, 7)) class TestSolrSimilarToRecid(InvenioTestCase): @@ -172,69 +122,37 @@ class TestSolrSimilarToRecid(InvenioTestCase): CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - ./bibrank -w wrd for all records """ - - def _get_similar_result_sequence(self, recid, rows=ROWS): - similar_result = solr_get_similar_ranked(recid, self._all_records, self._get_similar_ranking_params(), rows) + def get_similar_result_sequence(self, recid, rows=ROWS): + similar_result = solr_get_similar_ranked(recid, + self._all_records, + {'cutoff_amount': 10000, + 'cutoff_time_ms': 2000, + 'find_similar_to_recid': { + 'more_results_factor': 5, + 'mlt_fl': 'mlt', + 'mlt_mintf': 0, + 'mlt_mindf': 0, + 'mlt_minwl': 0, + 'mlt_maxwl': 0, + 'mlt_maxqt': 25, + 'mlt_maxntp': 1000, + 'mlt_boost': 'false' + } + }, + rows) return tuple([pair[0] for pair in similar_result[0]])[-rows:] - def _get_similar_topN(self, n): - return get_topN(n, self._SIMILAR) - - _SIMILAR = { - 30: (12, 95, 85, 82, 44, 1, 89, 64, 58, 15, 96, 61, 50, 86, 78, 77, 65, 62, 60, - 47, 46, 100, 99, 102, 91, 80, 7, 5, 92, 88, 74, 57, 55, 108, 84, 81, 79, 54, - 101, 11, 103, 94, 48, 83, 72, 63, 2, 68, 51, 53, 97, 93, 70, 45, 52, 14, - 59, 6, 10, 32, 33, 29, 30), - 59: (17, 69, 3, 20, 109, 14, 22, 33, 28, 24, 60, 6, 73, 113, 5, 107, 78, 4, 13, - 8, 45, 72, 74, 46, 104, 63, 71, 44, 87, 70, 103, 92, 57, 49, 7, 88, 68, 77, - 62, 10, 93, 2, 65, 55, 43, 94, 96, 1, 11, 99, 91, 61, 51, 15, 64, 97, 89, 101, - 108, 80, 86, 90, 54, 95, 102, 47, 100, 79, 83, 48, 12, 81, 82, 58, 50, 56, 84, - 85, 53, 52, 59) - } - - def _get_similar_ranking_params(self, cutoff_amount=10000, cutoff_time=2000): - """ - Default values from template_word_similarity_solr.cfg - """ - return { - 'cutoff_amount': cutoff_amount, - 'cutoff_time_ms': cutoff_time, - 'find_similar_to_recid': { - 'more_results_factor': 5, - 'mlt_fl': 'mlt', - 'mlt_mintf': 0, - 'mlt_mindf': 0, - 'mlt_minwl': 0, - 'mlt_maxwl': 0, - 'mlt_maxqt': 25, - 'mlt_maxntp': 1000, - 'mlt_boost': 'false' - } - } - _all_records = get_collection_reclist(CFG_SITE_NAME) + def setUp(self): + init_Solr() + @nottest def test_get_similar_ranked(self): """solrutils - similar results""" - all_ranked = 0 - similar_top = self._get_similar_topN(all_ranked) - recid = 30 - self.assertEqual(similar_top[recid], self._get_similar_result_sequence(recid=recid)) - recid = 59 - self.assertEqual(similar_top[recid], self._get_similar_result_sequence(recid=recid)) - - @nottest - def test_get_similar_ranked_top(self): - """solrutils - similar top results""" - top_n = 5 - similar_top = self._get_similar_topN(top_n) - recid = 30 - self.assertEqual(similar_top[recid], self._get_similar_result_sequence(recid=recid, rows=top_n)) - recid = 59 - self.assertEqual(similar_top[recid], self._get_similar_result_sequence(recid=recid, rows=top_n)) + self.assertEqual(self.get_similar_result_sequence(1), (5, 4, 7, 8, 3, 6, 2, 10, 1)) + self.assertEqual(self.get_similar_result_sequence(8), (3, 6, 9, 7, 2, 4, 5, 1, 10, 8)) class TestSolrWebSearch(InvenioTestCase): @@ -242,31 +160,24 @@ class TestSolrWebSearch(InvenioTestCase): make install-solrutils CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column - AND EITHER - Solr index built: ./bibindex -w fulltext for all records - OR - WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - and ./bibrank -w wrd for all records """ + def setUp(self): + init_Solr() @nottest def test_get_result(self): """solrutils - web search results""" self.assertEqual([], test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3AWillnotfind&rg=100', - expected_text="[]")) - - self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Ahiggs&rg=100', - expected_text="[12, 47, 48, 51, 52, 55, 56, 58, 68, 79, 80, 81, 85, 89, 96]")) + expected_text='[]')) self.assertEqual([], test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Aof&rg=100', - expected_text="[8, 10, 11, 12, 15, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 64, 68, 74, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97]")) + expected_text='[1, 2, 7, 8, 10]')) self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3A%22higgs+boson%22&rg=100', - expected_text="[12, 47, 51, 55, 56, 68, 81, 85]")) + test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3A%22of+the%22&rg=100', + expected_text='[1, 2, 7, 8, 10]')) class TestSolrWebRanking(InvenioTestCase): @@ -274,42 +185,25 @@ class TestSolrWebRanking(InvenioTestCase): make install-solrutils CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column - AND EITHER - Solr index built: ./bibindex -w fulltext for all records - OR - WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - and ./bibrank -w wrd for all records + WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg """ + def setUp(self): + init_Solr() @nottest def test_get_ranked(self): """solrutils - web ranking results""" self.assertEqual([], test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3AWillnotfind&rg=100&rm=wrd', - expected_text="[]")) + expected_text='[]')) - self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Ahiggs&rm=wrd', - expected_text="[12, 51, 79, 80, 81, 55, 47, 56, 96, 58, 68, 52, 48, 89, 85]")) - - self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Ahiggs&rg=100&rm=wrd', - expected_text="[12, 80, 81, 79, 51, 55, 47, 56, 96, 58, 68, 52, 48, 89, 85]")) - - # Record 77 is restricted self.assertEqual([], test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Aof&rm=wrd', - expected_text="[8, 10, 15, 43, 44, 45, 46, 48, 49, 51, 52, 53, 54, 55, 56, 57, 58, 60, 61, 62, 64, 68, 74, 78, 79, 81, 82, 83, 84, 85, 88, 89, 90, 91, 92, 95, 96, 97, 86, 11, 80, 93, 77, 12, 59, 87, 47, 94]", - username='admin')) + expected_text='[8, 2, 1, 10, 7]')) self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3Aof&rg=100&rm=wrd', - expected_text="[61, 60, 54, 56, 53, 10, 68, 44, 57, 83, 95, 92, 91, 74, 45, 48, 62, 82, 49, 51, 89, 90, 96, 43, 8, 64, 97, 15, 85, 78, 46, 55, 79, 84, 88, 81, 52, 58, 86, 11, 80, 93, 77, 12, 59, 87, 47, 94]", - username='admin')) - - self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3A%22higgs+boson%22&rg=100&rm=wrd', - expected_text="[12, 47, 51, 68, 81, 85, 55, 56]")) + test_web_page_content(CFG_SITE_URL + '/search?of=id&p=fulltext%3A%22of+the%22&rg=100&rm=wrd', + expected_text='[8, 10, 1, 2, 7]')) class TestSolrWebSimilarToRecid(InvenioTestCase): @@ -318,19 +212,20 @@ class TestSolrWebSimilarToRecid(InvenioTestCase): CFG_SOLR_URL set fulltext index in idxINDEX containing 'SOLR' in indexer column WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg - ./bibrank -w wrd for all records """ + def setUp(self): + init_Solr() @nottest def test_get_similar_ranked(self): """solrutils - web similar results""" self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=recid%3A30&rm=wrd', - expected_text="[1, 3, 4, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 34, 43, 44, 49, 50, 56, 58, 61, 64, 66, 67, 69, 71, 73, 75, 76, 77, 78, 82, 85, 86, 87, 89, 90, 95, 96, 98, 104, 107, 109, 113, 65, 62, 60, 47, 46, 100, 99, 102, 91, 80, 7, 5, 92, 88, 74, 57, 55, 108, 84, 81, 79, 54, 101, 11, 103, 94, 48, 83, 72, 63, 2, 68, 51, 53, 97, 93, 70, 45, 52, 14, 59, 6, 10, 32, 33, 29, 30]")) + test_web_page_content(CFG_SITE_URL + '/search?of=id&p=recid%3A1&rm=wrd', + expected_text='[9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 107, 108, 109, 113, 5, 4, 7, 8, 3, 6, 2, 10, 1]')) self.assertEqual([], - test_web_page_content(CFG_SITE_URL + '/search?of=id&p=recid%3A30&rg=100&rm=wrd', - expected_text="[3, 4, 8, 9, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 34, 43, 49, 56, 66, 67, 69, 71, 73, 75, 76, 87, 90, 98, 104, 107, 109, 113, 12, 95, 85, 82, 44, 1, 89, 64, 58, 15, 96, 61, 50, 86, 78, 77, 65, 62, 60, 47, 46, 100, 99, 102, 91, 80, 7, 5, 92, 88, 74, 57, 55, 108, 84, 81, 79, 54, 101, 11, 103, 94, 48, 83, 72, 63, 2, 68, 51, 53, 97, 93, 70, 45, 52, 14, 59, 6, 10, 32, 33, 29, 30]")) + test_web_page_content(CFG_SITE_URL + '/search?of=id&p=recid%3A8&rg=100&rm=wrd', + expected_text='[11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 107, 108, 109, 113, 3, 6, 9, 7, 2, 4, 5, 1, 10, 8]')) class TestSolrLoadLogicalFieldSettings(InvenioTestCase): @@ -339,7 +234,6 @@ class TestSolrLoadLogicalFieldSettings(InvenioTestCase): CFG_SOLR_URL set WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg """ - @nottest def test_load_logical_fields(self): """solrutils - load logical fields""" @@ -359,7 +253,6 @@ class TestSolrBuildFieldContent(InvenioTestCase): CFG_SOLR_URL set WRD method referring to Solr: /etc/bibrank$ cp template_word_similarity_solr.cfg wrd.cfg """ - @nottest def test_build_default_field_content(self): """solrutils - build default field content""" @@ -382,7 +275,6 @@ def test_build_custom_field_content(self): self.assertEqual(u"""In 1962, CERN hosted the 11th International Conference on High Energy Physics. Among the distinguished visitors were eight Nobel prizewinners.Left to right: Cecil F. Powell, Isidor I. Rabi, Werner Heisenberg, Edwin M. McMillan, Emile Segre, Tsung Dao Lee, Chen Ning Yang and Robert Hofstadter. En 1962, le CERN est l'hote de la onzieme Conference Internationale de Physique des Hautes Energies. Parmi les visiteurs eminents se trouvaient huit laureats du prix Nobel.De gauche a droite: Cecil F. Powell, Isidor I. Rabi, Werner Heisenberg, Edwin M. McMillan, Emile Segre, Tsung Dao Lee, Chen Ning Yang et Robert Hofstadter.""", get_field_content_in_utf8(6, 'abstract', tags)) - TESTS = [] From a3ecf52bfabf643742932cb92eeb1f025653f003 Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Tue, 25 Feb 2014 18:17:13 +0100 Subject: [PATCH 04/83] WebSearch: parsing unbalanced phrase queries * Removes unbalanced double quotation marks. Signed-off-by: Patrick Glauner --- modules/websearch/lib/search_engine.py | 6 ++++++ .../websearch/lib/search_engine_unit_tests.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index ab15175be8..047b0fd9da 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -920,6 +920,12 @@ def create_basic_search_units(req, p, f, m=None, of='hb'): if of.startswith("h"): write_warning("Ignoring empty %s search term." % fi, "Warning", req=req) del opfts[i] + # Handles phrases that lack ending double quote + if (pi.startswith('"') and not pi.endswith('"')) or (not pi.startswith('"') and pi.endswith('"')): + oi, pi, fi, m = opfts[i] + opfts[i] = [oi, pi.replace('"', ''), fi, m] + if of.startswith("h"): + write_warning("Ignoring incorrect phrase double quotation marks.", "Warning", req=req) except: pass diff --git a/modules/websearch/lib/search_engine_unit_tests.py b/modules/websearch/lib/search_engine_unit_tests.py index 8db84a91c5..31dbbdcca2 100644 --- a/modules/websearch/lib/search_engine_unit_tests.py +++ b/modules/websearch/lib/search_engine_unit_tests.py @@ -116,7 +116,22 @@ def test_parsing_exact_phrase_query(self): def test_parsing_exact_phrase_query_unbalanced(self): "search engine - parsing unbalanced exact phrase" self._check('"the word', 'title', None, - [['+', '"the', 'title', 'w'], + [['+', 'the', 'title', 'w'], + ['+', 'word', 'title', 'w']]) + self._check('the" word', 'title', None, + [['+', 'the', 'title', 'w'], + ['+', 'word', 'title', 'w']]) + self._check('"the"one" word', 'title', None, + [['+', 'theone', 'title', 'a'], + ['+', 'word', 'title', 'w']]) + self._check('"theone" word', 'title', None, + [['+', 'theone', 'title', 'a'], + ['+', 'word', 'title', 'w']]) + self._check('"t\'heone" word', 'title', None, + [['+', "t'heone", 'title', 'a'], + ['+', 'word', 'title', 'w']]) + self._check('"t\"heone" word', 'title', None, + [['+', 'theone', 'title', 'a'], ['+', 'word', 'title', 'w']]) def test_parsing_exact_phrase_query_in_any_field(self): From d7532a654255e77c1c8dc7bfe049ba21fe1baad8 Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Mon, 5 Aug 2013 19:06:19 +0200 Subject: [PATCH 05/83] WebSearch: new fulltext by default search * Adds checkbox to search terms also in to fulltext to the add to search interface. Signed-off-by: Patrick Glauner --- modules/websearch/lib/search_engine.py | 21 ++++++++++++- modules/websearch/lib/websearch_templates.py | 32 +++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index ab15175be8..8f46488af4 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -6254,9 +6254,28 @@ def prs_simple_search(results_in_any_collection, kwargs=None, req=None, of=None, # recommendations when there are results only in the hosted collections. Also added the if clause to avoid # searching in case we know we only have actual or potential hosted collections results if not only_hosted_colls_actual_or_potential_results_p: - results_in_any_collection.union_update(search_pattern_parenthesised(req, p, f, ap=ap, of=of, verbose=verbose, ln=ln, + # Full-text by default in add to search + if kwargs['aas'] == 2 and f=='fulltext': + # Displays nearest terms box only for the latter part to avoid display of box for metadata in case + # there are only results in the fulltext + results_in_any_collection.union_update(search_pattern_parenthesised(req, p, '', ap=ap, of=of, verbose=verbose, ln=ln, + display_nearest_terms_box=False, + wl=wl)) + # Change pattern to get results from fulltext per term + p_no_fields = re.sub('[a-z]*:', '', p) + p_no_fields = p_no_fields.replace('+', '|') + p_no_fields = p_no_fields.replace('AND', 'OR') + if verbose: + write_warning('Search stage 3: p is "%s"' % p, req=req) + write_warning('Search stage 3: p without fields for fulltext by default search is "%s"' % p_no_fields, req=req) + results_in_any_collection.union_update(search_pattern_parenthesised(req, p_no_fields, 'fulltext', ap=ap, of=of, verbose=verbose, ln=ln, + display_nearest_terms_box=not hosted_colls_actual_or_potential_results_p, + wl=wl)) + else: + results_in_any_collection.union_update(search_pattern_parenthesised(req, p, f, ap=ap, of=of, verbose=verbose, ln=ln, display_nearest_terms_box=not hosted_colls_actual_or_potential_results_p, wl=wl)) + except: register_exception(req=req, alert_admin=True) if of.startswith("h"): diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 2c54ea5596..bc4bc1c4e7 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -1159,7 +1159,7 @@ def tmpl_searchfor_addtosearch(self, ln, collection_id, record_count, searchwith return out - def create_addtosearch_box(self, ln, p, p1, searchwithin, header, adv_search_link): + def create_addtosearch_box(self, ln, p, p1, searchwithin, header, adv_search_link, f=''): """ Creates the Search and the Add-to-Search box. @@ -1249,6 +1249,8 @@ def create_addtosearch_box(self, ln, p, p1, searchwithin, header, adv_search_lin %(searchwithin1)s +
+ %(fulltext)s @@ -1262,8 +1264,9 @@ def create_addtosearch_box(self, ln, p, p1, searchwithin, header, adv_search_lin 'matchbox1' : self.tmpl_matchtype_box('m1', '', ln=ln), 'sizepattern' : CFG_WEBSEARCH_ADVANCEDSEARCH_PATTERN_BOX_WIDTH, 'searchwithin1' : searchwithin, - 'add_to_search' : _("Add to Search") - } + 'add_to_search' : _("Add to Search"), + 'fulltext': self.tmpl_fulltext(f, ln='en'), + } return out @@ -1348,6 +1351,26 @@ def tmpl_andornot_box(self, name='op', value='', ln='en'): } return out + def tmpl_fulltext(self, f, ln='en'): + """ + Returns HTML code for the search in fulltext checkbox. + + Parameters: + + - 'f' *string* - The value of the f paramater + + - 'ln' *string* - the language to display + """ + + _ = gettext_set_language(ln) + + checked = 'unchecked' + if f == 'fulltext': + checked = 'checked' + + return """ %(text)s""" % {'checked': checked, + 'text': _('Search also in the full-text of all documents')} + def tmpl_inputdate(self, name, ln, sy=0, sm=0, sd=0): """ Produces *From Date*, *Until Date* kind of selection box. Suitable for search options. @@ -2167,10 +2190,11 @@ def tmpl_search_box(self, ln, aas, cc, cc_intl, ot, sp, selected = '', values = self._add_mark_to_field(value=f1, fields=fieldslist, ln=ln) ) + adv_search_link = create_html_link(self.build_search_url(rm=rm, aas=1, cc=cc, jrec=jrec, ln=ln, rg=rg), {}, _("Advanced Search")) - out += self.create_addtosearch_box(ln, p, p1, searchwithin, '', adv_search_link) + out += self.create_addtosearch_box(ln, p, p1, searchwithin, '', adv_search_link, f) elif aas == 1: # print Advanced Search form: From 6eb6a554eae8a4ceae7191960fed155059342090 Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Mon, 26 Aug 2013 15:39:24 +0200 Subject: [PATCH 06/83] BibRank: Solr: better ranking for fulltext * Ranks fulltext and metadata properly by default. Signed-off-by: Patrick Glauner --- modules/bibrank/lib/bibrank_record_sorter.py | 4 ++-- modules/miscutil/lib/solrutils_bibrank_searcher.py | 5 ++++- modules/websearch/lib/search_engine.py | 11 ++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/bibrank/lib/bibrank_record_sorter.py b/modules/bibrank/lib/bibrank_record_sorter.py index 05baa868d5..c9e71400ba 100644 --- a/modules/bibrank/lib/bibrank_record_sorter.py +++ b/modules/bibrank/lib/bibrank_record_sorter.py @@ -222,7 +222,7 @@ def citation(rank_method_code, related_to, hitset, rank_limit_relevance, verbose return rank_by_citations(hits, verbose) -def rank_records(rank_method_code, rank_limit_relevance, hitset, related_to=[], verbose=0, field='', rg=None, jrec=None): +def rank_records(rank_method_code, rank_limit_relevance, hitset, related_to=[], verbose=0, field='', rg=None, jrec=None, kwargs={}): """Sorts given records or related records according to given method Parameters: @@ -293,7 +293,7 @@ def rank_records(rank_method_code, rank_limit_relevance, hitset, related_to=[], if function == "word_similarity_solr": if verbose > 0: voutput += "In Solr part:
" - result = word_similarity_solr(related_to, hitset, METHODS[rank_method_code], verbose, field, ranked_result_amount) + result = word_similarity_solr(related_to, hitset, METHODS[rank_method_code], verbose, field, ranked_result_amount, kwargs) if function == "word_similarity_xapian": if verbose > 0: voutput += "In Xapian part:
" diff --git a/modules/miscutil/lib/solrutils_bibrank_searcher.py b/modules/miscutil/lib/solrutils_bibrank_searcher.py index 28aa607467..eec7c3a92e 100644 --- a/modules/miscutil/lib/solrutils_bibrank_searcher.py +++ b/modules/miscutil/lib/solrutils_bibrank_searcher.py @@ -115,7 +115,7 @@ def get_normalized_ranking_scores(response, hitset_filter = None, recids = []): return (ranked_result, matched_recs) -def word_similarity_solr(pattern, hitset, params, verbose, explicit_field, ranked_result_amount): +def word_similarity_solr(pattern, hitset, params, verbose, explicit_field, ranked_result_amount, kwargs={}): """ Ranking a records containing specified words and returns a sorted list. input: @@ -138,6 +138,9 @@ def word_similarity_solr(pattern, hitset, params, verbose, explicit_field, ranke if pattern: pattern = " ".join(map(str, pattern)) from invenio.search_engine import create_basic_search_units + # Rank global index for fulltext by default in add to search + if kwargs.get('aas', 0) == 2 and explicit_field == 'fulltext': + explicit_field = '' search_units = create_basic_search_units(None, pattern, explicit_field) else: return (None, "Records not ranked. The query is not detailed enough, or not enough records found, for ranking to be possible.", "", voutput) diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index 8f46488af4..e8ccb82b89 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -4159,7 +4159,7 @@ def get_tags_from_sort_fields(sort_fields): return tags, '' -def rank_records(req, rank_method_code, rank_limit_relevance, hitset_global, pattern=None, verbose=0, sort_order='d', of='hb', ln=CFG_SITE_LANG, rg=None, jrec=None, field='', sorting_methods=SORTING_METHODS): +def rank_records(req, rank_method_code, rank_limit_relevance, hitset_global, pattern=None, verbose=0, sort_order='d', of='hb', ln=CFG_SITE_LANG, rg=None, jrec=None, field='', sorting_methods=SORTING_METHODS, kwargs={}): """Initial entry point for ranking records, acts like a dispatcher. (i) rank_method_code is in bsrMETHOD, bibsort buckets can be used; (ii)rank_method_code is not in bsrMETHOD, use bibrank; @@ -4193,7 +4193,8 @@ def rank_records(req, rank_method_code, rank_limit_relevance, hitset_global, pat field=field, related_to=related_to, rg=rg, - jrec=jrec) + jrec=jrec, + kwargs=kwargs) # Solution recs can be None, in case of error or other cases # which should be all be changed to return an empty list. @@ -6498,7 +6499,7 @@ def prs_print_records(kwargs=None, results_final=None, req=None, of=None, cc=Non results_final_recIDs_ranked, results_final_relevances, results_final_relevances_prologue, results_final_relevances_epilogue, results_final_comments = \ rank_records(req, rm, 0, results_final[coll], string.split(p) + string.split(p1) + - string.split(p2) + string.split(p3), verbose, so, of, ln, rg, jrec, kwargs['f']) + string.split(p2) + string.split(p3), verbose, so, of, ln, rg, jrec, kwargs['f'], kwargs=kwargs) if of.startswith("h"): write_warning(results_final_comments, req=req) if results_final_recIDs_ranked: @@ -6823,7 +6824,7 @@ def prs_display_results(kwargs=None, results_final=None, req=None, of=None, sf=N if rm: # do we have to rank? results_final_for_all_colls_rank_records_output = rank_records(req, rm, 0, results_final_for_all_selected_colls, p.split() + p1.split() + - p2.split() + p3.split(), verbose, so, of, ln, kwargs['rg'], kwargs['jrec'], kwargs['f']) + p2.split() + p3.split(), verbose, so, of, ln, kwargs['rg'], kwargs['jrec'], kwargs['f'], kwargs=kwargs) if results_final_for_all_colls_rank_records_output[0]: recIDs = results_final_for_all_colls_rank_records_output[0] elif sf or (CFG_BIBSORT_ENABLED and SORTING_METHODS): # do we have to sort? @@ -6878,7 +6879,7 @@ def prs_rank_results(kwargs=None, results_final=None, req=None, colls_to_search= if rm: # do we have to rank? results_final_for_all_colls_rank_records_output = rank_records(req, rm, 0, results_final_for_all_selected_colls, p.split() + p1.split() + - p2.split() + p3.split(), verbose, so, of, field=kwargs['f']) + p2.split() + p3.split(), verbose, so, of, field=kwargs['f'], kwargs=kwargs) if results_final_for_all_colls_rank_records_output[0]: recIDs = results_final_for_all_colls_rank_records_output[0] elif sf or (CFG_BIBSORT_ENABLED and SORTING_METHODS): # do we have to sort? From 7f4218c9da94aebba86a83ba9a7d45812603d61c Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Mon, 26 Aug 2013 15:41:39 +0200 Subject: [PATCH 07/83] SolrUtils: more accurate ranking scores * Ceils ranking scores to avoid anomalies. Signed-off-by: Patrick Glauner --- modules/miscutil/lib/solrutils_bibrank_searcher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/miscutil/lib/solrutils_bibrank_searcher.py b/modules/miscutil/lib/solrutils_bibrank_searcher.py index eec7c3a92e..0ac8ea2bab 100644 --- a/modules/miscutil/lib/solrutils_bibrank_searcher.py +++ b/modules/miscutil/lib/solrutils_bibrank_searcher.py @@ -25,6 +25,7 @@ import itertools +from math import ceil from invenio.config import CFG_SOLR_URL from invenio.intbitset import intbitset from invenio.errorlib import register_exception @@ -106,7 +107,11 @@ def get_normalized_ranking_scores(response, hitset_filter = None, recids = []): if (not hitset_filter and hitset_filter != []) or recid in hitset_filter or recid in recids: normalised_score = 0 if max_score > 0: - normalised_score = int(100.0 / max_score * float(hit['score'])) + # Ceil score, in particular beneficial for scores in (0,1) and (99,100) to take 1 and 100 + normalised_score = int(ceil(100.0 / max_score * float(hit['score']))) + # Correct possible rounding error + if normalised_score > 100: + normalised_score = 100 ranked_result.append((recid, normalised_score)) matched_recs.add(recid) From dafbee233fb4d776d83c12570d69af048b04cfc9 Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Fri, 26 Jul 2013 20:35:26 +0200 Subject: [PATCH 08/83] BibRecord: new MARC code filter * Filters a record based on MARC code. * Supports wildcards in identifiers and subfield code. Signed-off-by: Patrick Glauner --- modules/bibrecord/lib/Makefile.am | 1 + modules/bibrecord/lib/bibrecord.py | 81 ++++++++++ .../lib/bibrecord_regression_tests.py | 144 ++++++++++++++++++ modules/bibrecord/lib/bibrecord_unit_tests.py | 43 +++++- 4 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 modules/bibrecord/lib/bibrecord_regression_tests.py diff --git a/modules/bibrecord/lib/Makefile.am b/modules/bibrecord/lib/Makefile.am index 5096a9f93f..ec938875c8 100644 --- a/modules/bibrecord/lib/Makefile.am +++ b/modules/bibrecord/lib/Makefile.am @@ -20,6 +20,7 @@ pylibdir = $(libdir)/python/invenio pylib_DATA = bibrecord_config.py \ bibrecord.py \ bibrecord_unit_tests.py \ + bibrecord_regression_tests.py \ xmlmarc2textmarc.py \ textmarc2xmlmarc.py diff --git a/modules/bibrecord/lib/bibrecord.py b/modules/bibrecord/lib/bibrecord.py index 546501aa7e..a4830488bb 100644 --- a/modules/bibrecord/lib/bibrecord.py +++ b/modules/bibrecord/lib/bibrecord.py @@ -55,6 +55,8 @@ # has been deleted. CFG_BIBRECORD_KEEP_SINGLETONS = True +CONTROL_TAGS = ('001', '003', '005', '006', '007', '008') + try: import pyRXP if 'pyrxp' in CFG_BIBRECORD_PARSERS_AVAILABLE: @@ -1465,6 +1467,85 @@ def _validate_record_field_positions_global(record): return ("Duplicate global field position '%d' in tag '%s'" % (field[4], tag)) +def get_marc_tag_extended_with_wildcards(tag): + """ + Adds wildcards to a non-control field tag, identifiers and subfieldcode and returns it. + Example: + 001 -> 001 + 595 -> 595%%% + 5955 -> 5955%% + 59555 -> 59555% + 59555a -> 59555a + """ + length = len(tag) + if length == 3 and tag not in CONTROL_TAGS: + return '%s%%%%%%' % tag + if length == 4: + return '%s%%%%' % tag + if length == 5: + return '%s%%' % tag + return tag + +def get_filtered_record(rec, marc_codes): + """ + Filters a record based on MARC code and returns a new filtered record. + Supports wildcards in ind1, ind2, subfieldcode. + Returns entire record if filter list is empty. + """ + if not marc_codes: + return rec + + res = {} + split_tags = map(_get_split_marc_code, marc_codes) + + for tag, ind1, ind2, subfieldcode in split_tags: + # Tag must exist + if tag in rec: + # Control field + if tag in CONTROL_TAGS: + value = record_get_field_value(rec, tag) + record_add_field(res, tag, ind1, ind2, controlfield_value=value) + # Simple subfield + elif '%' not in (ind1, ind2, subfieldcode): + values = record_get_field_values(rec, tag, ind1, ind2, subfieldcode) + for value in values: + record_add_field(res, tag, ind1, ind2, subfields=[(subfieldcode, value)]) + # Wildcard in ind1, ind2 or subfield + elif '%' in (ind1, ind2, subfieldcode): + field_instances = record_get_field_instances(rec, tag, ind1, ind2) + for entity in field_instances: + subfields = [] + if subfieldcode == '%': + subfields = entity[0] + else: + for subfield in entity[0]: + if subfield[0] == subfieldcode: + subfields.append(subfield) + if len(subfields): + record_add_field(res, tag, entity[1], entity[2], subfields=subfields) + + return res + +def _get_split_marc_code(marc_code): + """ + Splits a MARC code into tag, ind1 ind2, and subfieldcode. + Accepts '_' values which are converted to ' '. + """ + tag, ind1, ind2, subfieldcode = '', '', '', '' + + length = len(marc_code) + + if length >= 3: + tag = marc_code[0:3] + if length >= 4: + ind1 = marc_code[3].replace('_', '') + if length >= 5: + ind2 = marc_code[4].replace('_', '') + if length == 6: + subfieldcode = marc_code[5].replace('_', '') + return tag, ind1, ind2, subfieldcode + + def _record_sort_by_indicators(record): """Sorts the fields inside the record by indicators.""" for tag, fields in record.items(): diff --git a/modules/bibrecord/lib/bibrecord_regression_tests.py b/modules/bibrecord/lib/bibrecord_regression_tests.py new file mode 100644 index 0000000000..54a1833023 --- /dev/null +++ b/modules/bibrecord/lib/bibrecord_regression_tests.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 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. + +""" +The BibRecord regression test suite. +""" + +import unittest + +from invenio.config import CFG_SITE_URL, \ + CFG_SITE_RECORD +from invenio import bibrecord +from invenio.testutils import make_test_suite, run_test_suite +from invenio.search_engine import get_record + + +class BibRecordFilterBibrecordTest(unittest.TestCase): + """ bibrecord - testing for code filtering""" + + def setUp(self): + self.rec = get_record(10) + + def test_empty_filter(self): + """bibrecord - empty filter""" + self.assertEqual(bibrecord.get_filtered_record(self.rec, []), self.rec) + + def test_filter_tag_only(self): + """bibrecord - filtering only by MARC tag""" + # Exist + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['001']), {'001': [([], ' ', ' ', '10', 1)]}) + # Do not exist + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['037']), {}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['856']), {}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['999']), {}) + # Sequence + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['001', '999']), {'001': [([], ' ', ' ', '10', 1)]}) + # Some tags do not exist + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['001', '260', '856', '400', '500', '999']), {'001': [([], ' ', ' ', '10', 1)]}) + + def test_filter_subfields(self): + """bibrecord - filtering subfields""" + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['65017a']), {'650': [([('a', 'Particle Physics - Experimental Results')], '1', '7', '', 1)],}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['65017a', '650172']), {'650': [([('a', 'Particle Physics - Experimental Results')], '1', '7', '', 1), + ([('2', 'SzGeCERN')], '1', '7', '', 2)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['8560_f']), {'856': [([('f', 'valerie.brunner@cern.ch')], '0', ' ', '', 1)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['260__a']), {'260': [([('a', 'Geneva')], ' ', ' ', '', 1)],}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['595__a']), {'595': [([('a', 'CERN EDS')], ' ', ' ', '', 1), + ([('a', '20011220SLAC')], ' ', ' ', '', 2), + ([('a', 'giva')], ' ', ' ', '', 3), + ([('a', 'LANL EDS')], ' ', ' ', '', 4)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['8564_u']), {'856': [([('u', '%s/%s/10/files/ep-2001-094.ps.gz' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 1), + ([('u', '%s/%s/10/files/ep-2001-094.pdf' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 2)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['595__a', '8564_u']), {'595': [([('a', 'CERN EDS')], ' ', ' ', '', 1), + ([('a', '20011220SLAC')], ' ', ' ', '', 2), + ([('a', 'giva')], ' ', ' ', '', 3), + ([('a', 'LANL EDS')], ' ', ' ', '', 4)], + '856': [([('u', '%s/%s/10/files/ep-2001-094.ps.gz' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 5), + ([('u', '%s/%s/10/files/ep-2001-094.pdf' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 6)]}) + + def test_filter_comprehensive(self): + """bibrecord - comprehensive filtering""" + tags = ['001', '035', '037__a', '65017a', '650'] + res = {} + res['001'] = [([], ' ', ' ', '10', 1)] + res['037'] = [([('a', 'hep-ex/0201013')], ' ', ' ', '', 2)] + res['650'] = [([('a', 'Particle Physics - Experimental Results')], '1', '7', '', 3)] + self.assertEqual(bibrecord.get_filtered_record(self.rec, tags), res) + + def test_filter_wildcards(self): + """bibrecord - wildcards filtering""" + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['595__%']), {'595': [([('a', 'CERN EDS')], ' ', ' ', '', 1), + ([('a', '20011220SLAC')], ' ', ' ', '', 2), + ([('a', 'giva')], ' ', ' ', '', 3), + ([('a', 'LANL EDS')], ' ', ' ', '', 4)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909CS%']), {'909': [([('s', 'n'), ('w', '200231')], 'C', 'S', '', 1)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['856%']), {}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['856%_u']), {'856': [([('u', '%s/%s/10/files/ep-2001-094.ps.gz' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 1), + ([('u', '%s/%s/10/files/ep-2001-094.pdf' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 2)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909%5v']), {}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909%5b']), {'909': [([('b', 'CER')], 'C', '5', '', 1)]}) + + def test_filter_multi_wildcards(self): + """bibrecord - multi wildcards filtering""" + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909%%_']), {}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['856%_%']), {'856': [([('f', 'valerie.brunner@cern.ch')], '0', ' ', '', 1), + ([('s', '217223'), ('u', '%s/%s/10/files/ep-2001-094.ps.gz' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 2), + ([('s', '383040'), ('u', '%s/%s/10/files/ep-2001-094.pdf' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 3)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909%%b']), {'909': [([('b', '11')], 'C', '0', '', 1), + ([('b', 'CER')], 'C', '5', '', 2)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['909%%%']), {'909': [([('y', '2002')], 'C', '0', '', 1), + ([('e', 'ALEPH')], 'C', '0', '', 2), + ([('b', '11')], 'C', '0', '', 3), + ([('p', 'EP')], 'C', '0', '', 4), + ([('a', 'CERN LEP')], 'C', '0', '', 5), + ([('c', '2001-12-19'), ('l', '50'), ('m', '2002-02-19'), ('o', 'BATCH')], 'C', '1', '', 6), + ([('u', 'CERN')], 'C', '1', '', 7), + ([('p', 'Eur. Phys. J., C')], 'C', '4', '', 8), + ([('b', 'CER')], 'C', '5', '', 9), + ([('s', 'n'), ('w', '200231')], 'C', 'S', '', 10), + ([('o', 'oai:cds.cern.ch:CERN-EP-2001-094'), ('p', 'cern:experiment')], 'C', 'O', '', 11)]}) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['980%%%']), bibrecord.get_filtered_record(self.rec, ['980_%%'])) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['980_%%']), bibrecord.get_filtered_record(self.rec, ['980%_%'])) + self.assertEqual(bibrecord.get_filtered_record(self.rec, ['980__%']), bibrecord.get_filtered_record(self.rec, ['980%%%'])) + + def test_filter_wildcard_comprehensive(self): + """bibrecord - comprehensive wildcard filtering""" + tags = ['595__%', '909CS%', '856%', '856%_%', '909%5b', '980%%%'] + res = {} + res['595'] = [([('a', 'CERN EDS')], ' ', ' ', '', 1), + ([('a', '20011220SLAC')], ' ', ' ', '', 2), + ([('a', 'giva')], ' ', ' ', '', 3), + ([('a', 'LANL EDS')], ' ', ' ', '', 4)] + res['856'] = [([('f', 'valerie.brunner@cern.ch')], '0', ' ', '', 5), + ([('s', '217223'), ('u', '%s/%s/10/files/ep-2001-094.ps.gz' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 6), + ([('s', '383040'), ('u', '%s/%s/10/files/ep-2001-094.pdf' % (CFG_SITE_URL, CFG_SITE_RECORD))], '4', ' ', '', 7)] + res['909'] = [([('s', 'n'), ('w', '200231')], 'C', 'S', '', 8), + ([('b', 'CER')], 'C', '5', '', 9)] + res['980'] = [([('a', 'PREPRINT')], ' ', ' ', '', 10), + ([('a', 'ALEPHPAPER')], ' ', ' ', '', 11)] + self.assertEqual(bibrecord.get_filtered_record(self.rec, tags), res) + + +TEST_SUITE = make_test_suite( + BibRecordFilterBibrecordTest, + ) + +if __name__ == '__main__': + run_test_suite(TEST_SUITE, warn_user=True) diff --git a/modules/bibrecord/lib/bibrecord_unit_tests.py b/modules/bibrecord/lib/bibrecord_unit_tests.py index 5c985c4e8a..e50065d321 100644 --- a/modules/bibrecord/lib/bibrecord_unit_tests.py +++ b/modules/bibrecord/lib/bibrecord_unit_tests.py @@ -18,15 +18,18 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ -The BibRecord test suite. +The BibRecord unit test suite. """ from invenio.testutils import InvenioTestCase from invenio.config import CFG_TMPDIR, \ - CFG_BIBUPLOAD_EXTERNAL_OAIID_TAG + CFG_BIBUPLOAD_EXTERNAL_OAIID_TAG, \ + CFG_SITE_URL, \ + CFG_SITE_RECORD from invenio import bibrecord, bibrecord_config from invenio.testutils import make_test_suite, run_test_suite +from invenio.search_engine import get_record try: import pyRXP @@ -1770,6 +1773,38 @@ def test_extract_oai_id(self): 'oai:atlantis:1') +class BibRecordSplitMARCCodeTest(InvenioTestCase): + """ bibrecord - testing for MARC code spliting""" + + def test_split_tag(self): + """bibrecord - splitting MARC code""" + self.assertEqual(bibrecord._get_split_marc_code('001'), ('001', '', '', '')) + self.assertEqual(bibrecord._get_split_marc_code('65017a'), ('650', '1', '7', 'a')) + self.assertEqual(bibrecord._get_split_marc_code('037__a'), ('037', '', '', 'a')) + self.assertEqual(bibrecord._get_split_marc_code('8560_f'), ('856', '0', '', 'f')) + self.assertEqual(bibrecord._get_split_marc_code('909C1'), ('909', 'C', '1', '')) + self.assertEqual(bibrecord._get_split_marc_code('650'), ('650', '', '', '')) + self.assertEqual(bibrecord._get_split_marc_code('650__%'), ('650', '', '', '%')) + self.assertEqual(bibrecord._get_split_marc_code('650%'), ('650', '%', '', '')) + + +class BibRecordExtensionWithWildcardsTagTest(InvenioTestCase): + """ bibrecord - testing for MARC tag extension with wildcards""" + + def test_split_tag(self): + """bibrecord - extending MARC tag with wildcards""" + # No valid tag + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('01'), '01') + # Control tag + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('001'), '001') + # Extending tag + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('595'), '595%%%') + # Extending tag and identifier(s) + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('0001'), '0001%%') + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('00012'), '00012%') + self.assertEqual(bibrecord.get_marc_tag_extended_with_wildcards('00012a'), '00012a') + + TEST_SUITE = make_test_suite( BibRecordSuccessTest, BibRecordParsersTest, @@ -1795,7 +1830,9 @@ def test_extract_oai_id(self): BibRecordSingletonTest, BibRecordNumCharRefTest, BibRecordExtractIdentifiersTest, - BibRecordDropDuplicateFieldsTest + BibRecordDropDuplicateFieldsTest, + BibRecordSplitMARCCodeTest, + BibRecordExtensionWithWildcardsTagTest, ) if __name__ == '__main__': From 5288081a0a9490e19d87cfe58e45bb3ac86ab0bb Mon Sep 17 00:00:00 2001 From: Patrick Glauner Date: Mon, 29 Jul 2013 18:03:54 +0200 Subject: [PATCH 09/83] BibFormat: new 'xmf' format * Supports filtering per MARC tag, identifier and subfield. Signed-off-by: Patrick Glauner --- .../etc/format_templates/MARCXML_FIELDED.bft | 3 +++ .../etc/format_templates/Makefile.am | 1 + .../bibformat/etc/output_formats/Makefile.am | 1 + modules/bibformat/etc/output_formats/XMF.bfo | 1 + modules/bibformat/lib/bibformat.py | 17 ++++++++----- modules/bibformat/lib/bibformat_engine.py | 24 ++++++++++++------- .../bibformat/lib/elements/bfe_xml_record.py | 17 +++++++++++-- modules/websearch/lib/search_engine.py | 5 ++-- 8 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 modules/bibformat/etc/format_templates/MARCXML_FIELDED.bft create mode 100644 modules/bibformat/etc/output_formats/XMF.bfo diff --git a/modules/bibformat/etc/format_templates/MARCXML_FIELDED.bft b/modules/bibformat/etc/format_templates/MARCXML_FIELDED.bft new file mode 100644 index 0000000000..4ac0175212 --- /dev/null +++ b/modules/bibformat/etc/format_templates/MARCXML_FIELDED.bft @@ -0,0 +1,3 @@ +MARC XML +Standard MARC XML output + diff --git a/modules/bibformat/etc/format_templates/Makefile.am b/modules/bibformat/etc/format_templates/Makefile.am index 5ef4d5b554..a8ebc1e9f2 100644 --- a/modules/bibformat/etc/format_templates/Makefile.am +++ b/modules/bibformat/etc/format_templates/Makefile.am @@ -51,6 +51,7 @@ etc_DATA = Default_HTML_captions.bft \ DataCite.xsl \ Default_Mobile_brief.bft \ Default_Mobile_detailed.bft \ + MARCXML_FIELDED.bft \ Authority_HTML_brief.bft \ People_HTML_detailed.bft \ People_HTML_brief.bft \ diff --git a/modules/bibformat/etc/output_formats/Makefile.am b/modules/bibformat/etc/output_formats/Makefile.am index 2878a3ed0c..041d742b3f 100644 --- a/modules/bibformat/etc/output_formats/Makefile.am +++ b/modules/bibformat/etc/output_formats/Makefile.am @@ -23,6 +23,7 @@ etc_DATA = HB.bfo \ HP.bfo \ HX.bfo \ XM.bfo \ + XMF.bfo \ EXCEL.bfo \ XP.bfo \ XN.bfo \ diff --git a/modules/bibformat/etc/output_formats/XMF.bfo b/modules/bibformat/etc/output_formats/XMF.bfo new file mode 100644 index 0000000000..1aa575d89e --- /dev/null +++ b/modules/bibformat/etc/output_formats/XMF.bfo @@ -0,0 +1 @@ +default: MARCXML_FIELDED.bft diff --git a/modules/bibformat/lib/bibformat.py b/modules/bibformat/lib/bibformat.py index 257a62d34c..058ba0a5bb 100644 --- a/modules/bibformat/lib/bibformat.py +++ b/modules/bibformat/lib/bibformat.py @@ -52,7 +52,7 @@ # def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, xml_record=None, user_info=None, on_the_fly=False, - save_missing=True, force_2nd_pass=False): + save_missing=True, force_2nd_pass=False, ot=''): """ Returns the formatted record with id 'recID' and format 'of' @@ -80,6 +80,8 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, @param recID: the id of the record to fetch @param of: the output format code + @param ot: output only these MARC tags (e.g. ['100', '999']), only supported for 'xmf' format + @type ot: list @return: formatted record as String, or '' if it does not exist """ out, needs_2nd_pass = bibformat_engine.format_record_1st_pass( @@ -91,7 +93,8 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, xml_record=xml_record, user_info=user_info, on_the_fly=on_the_fly, - save_missing=save_missing) + save_missing=save_missing, + ot=ot) if needs_2nd_pass or force_2nd_pass: out = bibformat_engine.format_record_2nd_pass( recID=recID, @@ -101,7 +104,8 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, verbose=verbose, search_pattern=search_pattern, xml_record=xml_record, - user_info=user_info) + user_info=user_info, + ot=ot) return out @@ -138,7 +142,7 @@ def record_get_xml(recID, format='xm', decompress=zlib.decompress): def format_records(recIDs, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, xml_records=None, user_info=None, record_prefix=None, record_separator=None, record_suffix=None, prologue="", - epilogue="", req=None, on_the_fly=False): + epilogue="", req=None, on_the_fly=False, ot=''): """ Format records given by a list of record IDs or a list of records as xml. Adds a prefix before each record, a suffix after each @@ -195,11 +199,12 @@ def format_records(recIDs, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, @param req: an optional request object where to print records @param on_the_fly: if False, try to return an already preformatted version of the record in the database @type on_the_fly: boolean + @param ot: output only these MARC tags (e.g. "100,700,909C0b"), only supported for 'xmf' format + @type ot: string @rtype: string """ if req is not None: req.write(prologue) - formatted_records = '' #Fill one of the lists with Nones @@ -229,7 +234,7 @@ def format_records(recIDs, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, #Print formatted record formatted_record = format_record(recIDs[i], of, ln, verbose, search_pattern, xml_records[i], - user_info, on_the_fly) + user_info, on_the_fly, ot=ot) formatted_records += formatted_record if req is not None: req.write(formatted_record) diff --git a/modules/bibformat/lib/bibformat_engine.py b/modules/bibformat/lib/bibformat_engine.py index fb98d5d33c..776b2369ff 100644 --- a/modules/bibformat/lib/bibformat_engine.py +++ b/modules/bibformat/lib/bibformat_engine.py @@ -197,7 +197,7 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, - search_pattern=None, xml_record=None, user_info=None): + search_pattern=None, xml_record=None, user_info=None, ot=''): """ Formats a record given output format. Main entry function of bibformat engine. @@ -224,6 +224,8 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, @param search_pattern: list of strings representing the user request in web interface @param xml_record: an xml string representing the record to format @param user_info: the information of the user who will view the formatted page + @param ot: output only these MARC tags (e.g. "100,700,909C0b"), only supported for 'xmf' format + @type ot: string @return: formatted record """ if search_pattern is None: @@ -239,7 +241,7 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, # But if format not found for new BibFormat, then call old BibFormat #Create a BibFormat Object to pass that contain record and context - bfo = BibFormatObject(recID, ln, search_pattern, xml_record, user_info, of) + bfo = BibFormatObject(recID, ln, search_pattern, xml_record, user_info, of, ot) if of.lower() != 'xm' and (not bfo.get_record() or record_empty(bfo.get_record())): @@ -290,7 +292,7 @@ def format_record(recID, of, ln=CFG_SITE_LANG, verbose=0, def format_record_1st_pass(recID, of, ln=CFG_SITE_LANG, verbose=0, search_pattern=None, xml_record=None, user_info=None, on_the_fly=False, - save_missing=True): + save_missing=True, ot=''): """ Format a record in given output format. @@ -362,7 +364,7 @@ def format_record_1st_pass(recID, of, ln=CFG_SITE_LANG, verbose=0, out += """\n
Found preformatted output for record %i (cache updated on %s).
""" % (recID, last_updated) - if of.lower() == 'xm': + if of.lower() in ('xm', 'xmf'): res = filter_hidden_fields(res, user_info) # try to replace language links in pre-cached res, if applicable: if ln != CFG_SITE_LANG and of.lower() in CFG_BIBFORMAT_DISABLE_I18N_FOR_CACHED_FORMATS: @@ -396,10 +398,11 @@ def format_record_1st_pass(recID, of, ln=CFG_SITE_LANG, verbose=0, verbose=verbose, search_pattern=search_pattern, xml_record=xml_record, - user_info=user_info) + user_info=user_info, + ot=ot) out += out_ - if of.lower() in ('xm', 'xoaimarc'): + if of.lower() in ('xm', 'xoaimarc', 'xmf'): out = filter_hidden_fields(out, user_info, force_filtering=of.lower()=='xoaimarc') # We have spent time computing this format @@ -443,9 +446,9 @@ def format_record_1st_pass(recID, of, ln=CFG_SITE_LANG, verbose=0, def format_record_2nd_pass(recID, template, ln=CFG_SITE_LANG, search_pattern=None, xml_record=None, - user_info=None, of=None, verbose=0): + user_info=None, of=None, verbose=0, ot=''): # Create light bfo object - bfo = BibFormatObject(recID, ln, search_pattern, xml_record, user_info, of) + bfo = BibFormatObject(recID, ln, search_pattern, xml_record, user_info, of, ot) # Translations template = translate_template(template, ln) # Format template @@ -1825,7 +1828,7 @@ class BibFormatObject(object): req = None # DEPRECATED: use bfo.user_info instead. Used by WebJournal. def __init__(self, recID, ln=CFG_SITE_LANG, search_pattern=None, - xml_record=None, user_info=None, output_format=''): + xml_record=None, user_info=None, output_format='', ot=''): """ Creates a new bibformat object, with given record. @@ -1855,6 +1858,8 @@ def __init__(self, recID, ln=CFG_SITE_LANG, search_pattern=None, @param xml_record: a xml string of the record to format @param user_info: the information of the user who will view the formatted page @param output_format: the output_format used for formatting this record + @param ot: output only these MARC tags (e.g. ['100', '999']), only supported for 'xmf' format + @type ot: list """ self.xml_record = None # *Must* remain empty if recid is given if xml_record is not None: @@ -1872,6 +1877,7 @@ def __init__(self, recID, ln=CFG_SITE_LANG, search_pattern=None, self.user_info = user_info if self.user_info is None: self.user_info = collect_user_info(None) + self.ot = ot def get_record(self): """ diff --git a/modules/bibformat/lib/elements/bfe_xml_record.py b/modules/bibformat/lib/elements/bfe_xml_record.py index 020987a902..e4c6c40d37 100644 --- a/modules/bibformat/lib/elements/bfe_xml_record.py +++ b/modules/bibformat/lib/elements/bfe_xml_record.py @@ -24,14 +24,27 @@ def format_element(bfo, type='xml', encodeForXML='yes'): """ Prints the complete current record as XML. - @param type: the type of xml. Can be 'xml', 'oai_dc', 'marcxml', 'xd' + @param type: the type of xml. Can be 'xml', 'oai_dc', 'marcxml', 'xd', 'xmf' @param encodeForXML: if 'yes', replace all < > and & with html corresponding escaped characters. """ from invenio.bibformat_utils import record_get_xml + from invenio.bibrecord import get_filtered_record, record_xml_output, get_marc_tag_extended_with_wildcards from invenio.textutils import encode_for_xml + from invenio.search_engine import get_record #Can be used to output various xml flavours. - out = record_get_xml(bfo.recID, format=type, on_the_fly=True) + out = '' + if type == 'xmf': + tags = bfo.ot + if tags != ['']: + filter_tags = map(get_marc_tag_extended_with_wildcards, tags) + else: + filter_tags = [] + record = get_record(bfo.recID) + filtered_record = get_filtered_record(record, filter_tags) + out = record_xml_output(filtered_record) + else: + out = record_get_xml(bfo.recID, format=type, on_the_fly=True) if encodeForXML.lower() == 'yes': return encode_for_xml(out) diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index ab15175be8..4c6e252c54 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -4547,7 +4547,7 @@ def print_records(req, recIDs, jrec=1, rg=CFG_WEBSEARCH_DEF_RECORDS_IN_GROUPS, f if print_records_prologue_p: print_records_prologue(req, format) - if ot: + if ot and not format.startswith('xmf'): # asked to print some filtered fields only, so call print_record() on the fly: for recid in recIDs: x = print_record(recid, @@ -4571,7 +4571,8 @@ def print_records(req, recIDs, jrec=1, rg=CFG_WEBSEARCH_DEF_RECORDS_IN_GROUPS, f search_pattern=search_pattern, record_separator="\n", user_info=user_info, - req=req) + req=req, + ot=ot) # print footer if needed if print_records_epilogue_p: From 48eee665ac52263d5e7e988f84b597906999cf1f Mon Sep 17 00:00:00 2001 From: Ludmila Marian Date: Tue, 12 Aug 2014 12:15:32 +0200 Subject: [PATCH 10/83] WebStyle: add CERN specific URL handlers * Adds six CERN specific URL handlers. Signed-off-by: Ludmila Marian --- modules/webstyle/lib/webinterface_layout.py | 63 ++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/modules/webstyle/lib/webinterface_layout.py b/modules/webstyle/lib/webinterface_layout.py index 494dbed8d6..96bc35fced 100644 --- a/modules/webstyle/lib/webinterface_layout.py +++ b/modules/webstyle/lib/webinterface_layout.py @@ -28,7 +28,10 @@ from invenio.errorlib import register_exception from invenio.webinterface_handler import WebInterfaceDirectory from invenio import webinterface_handler_config as apache -from invenio.config import CFG_DEVEL_SITE, CFG_OPENAIRE_SITE, CFG_ACCESS_CONTROL_LEVEL_SITE +from invenio.config import CFG_DEVEL_SITE, \ + CFG_OPENAIRE_SITE, \ + CFG_ACCESS_CONTROL_LEVEL_SITE, \ + CFG_CERN_SITE class WebInterfaceDisabledPages(WebInterfaceDirectory): @@ -301,6 +304,47 @@ def _lookup(self, component, path): register_exception(alert_admin=True, subject='EMERGENCY') WebInterfaceAuthorlistPages = WebInterfaceDumbPages +if CFG_CERN_SITE: + try: + from invenio.aleph_webinterface import WebInterfaceAlephPages + except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceAlephPages = WebInterfaceDumbPages + + try: + from invenio.setlink_webinterface import WebInterfaceSetLinkPages + except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceSetLinkPages = WebInterfaceDumbPages + + try: + from invenio.yellowreports_webinterface import WebInterfaceYellowReportsPages + except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceYellowReportsPages = WebInterfaceDumbPages + + try: + from invenio.webimages_webinterface import WebInterfaceImagesPages + except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceImagesPages = WebInterfaceDumbPages + + try: + from invenio.embedvideo_webinterface import WebInterfaceEmbedVideo + except: + register_exception(alert_admin=True, subject='EMERGENCE') + WebInterfaceEmbedVideo = WebInterfaceDumbPages + + try: + from invenio.webapi_webinterface import WebInterfaceAPIPages + except: + register_exception(alert_admin=True, subject='EMERGENCE') + WebInterfaceAPIPages = WebInterfaceDumbPages + + cds_exports = ['cdslib', 'setlink', 'images', 'video', 'api', 'yellowrep'] +else: + cds_exports = [] + if CFG_OPENAIRE_SITE: try: from invenio.openaire_deposit_webinterface import \ @@ -369,7 +413,7 @@ class WebInterfaceInvenio(WebInterfaceSearchInterfacePages): 'goto', 'info', 'authorlist', - ] + test_exports + openaire_exports + ] + test_exports + openaire_exports + cds_exports def __init__(self): self.getfile = bibdocfile_legacy_getfile @@ -410,6 +454,14 @@ def __init__(self): yourcomments = WebInterfaceDisabledPages() goto = WebInterfaceDisabledPages() authorlist = WebInterfaceDisabledPages() + if CFG_CERN_SITE: + cdslib = WebInterfaceDisabledPages() + setlink = WebInterfaceDisabledPages() + yellowrep = WebInterfaceYellowReportsPages() + images = WebInterfaceImagesPages() + video = WebInterfaceEmbedVideo() + api = WebInterfaceAPIPages() + else: submit = WebInterfaceSubmitPages() youraccount = WebInterfaceYourAccountPages() @@ -442,6 +494,13 @@ def __init__(self): yourcomments = WebInterfaceYourCommentsPages() goto = WebInterfaceGotoPages() authorlist = WebInterfaceAuthorlistPages() + if CFG_CERN_SITE: + cdslib = WebInterfaceAlephPages() + setlink = WebInterfaceSetLinkPages() + yellowrep = WebInterfaceYellowReportsPages() + images = WebInterfaceImagesPages() + video = WebInterfaceEmbedVideo() + api = WebInterfaceAPIPages() # This creates the 'handler' function, which will be invoked directly From 54509c109ed736b945c7b70a82cb0098b9116fc2 Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis Date: Tue, 7 Jan 2014 16:04:53 +0100 Subject: [PATCH 11/83] BibEncode: Video: Push video to youtube * Push video to youtube (closes #1664) * Fixes to support CERN SITES * Fix authentication result object check Signed-off-by: Harris Tzovanakis --- Makefile.am | 32 + modules/bibencode/etc/Makefile.am | 3 +- modules/bibencode/etc/client_secrets.json | 13 + modules/bibencode/lib/Makefile.am | 3 +- modules/bibencode/lib/bibencode_config.py | 25 +- modules/bibencode/lib/bibencode_youtube.py | 611 ++++++++++++++++++ .../bibencode/www/video_platform_record.css | 9 +- .../etc/format_templates/Makefile.am | 2 +- .../format_templates/Video_HTML_detailed.bft | 7 +- modules/bibformat/lib/elements/Makefile.am | 3 +- .../lib/elements/bfe_youtube_authorization.py | 39 ++ modules/webstyle/css/Makefile.am | 3 +- modules/webstyle/css/youtube.css | 174 +++++ modules/webstyle/img/Makefile.am | 10 +- modules/webstyle/img/sign_in_with_google.png | Bin 0 -> 4975 bytes .../webstyle/img/youtube-logo-icon-24px.png | Bin 0 -> 1699 bytes modules/webstyle/lib/webinterface_layout.py | 10 +- 17 files changed, 925 insertions(+), 19 deletions(-) create mode 100644 modules/bibencode/etc/client_secrets.json create mode 100644 modules/bibencode/lib/bibencode_youtube.py create mode 100644 modules/bibformat/lib/elements/bfe_youtube_authorization.py create mode 100644 modules/webstyle/css/youtube.css create mode 100644 modules/webstyle/img/sign_in_with_google.png create mode 100755 modules/webstyle/img/youtube-logo-icon-24px.png diff --git a/Makefile.am b/Makefile.am index 6f44617c95..5989e66061 100644 --- a/Makefile.am +++ b/Makefile.am @@ -324,6 +324,38 @@ uninstall-pdfa-helper-files: @echo "** The PDF/A helper files were successfully uninstalled. **" @echo "***********************************************************" +install-youtube: + @echo "***********************************************************" + @echo "** Installing youtube client libraries **" + @echo "***********************************************************" + @echo "Please make sure that you have pip installed **" + @echo "-----------------------------------------------------------" + @echo "For more infos about the library please visit:" + @echo "https://developers.google.com/api-client-library/python/start/installation" + sudo pip install --upgrade google-api-python-client + rm -rf /tmp/invenio_js_frameworks + mkdir -p /tmp/invenio_js_frameworks + (cd /tmp/invenio_js_frameworks && \ + wget https://github.com/dimsemenov/Magnific-Popup/archive/master.zip && \ + unzip master.zip && \ + mkdir -p ${prefix}/var/www/static/magnific_popup && \ + cp -r Magnific-Popup-master/dist/* ${prefix}/var/www/static/magnific_popup && \ + cd /tmp && \ + rm -rf invenio_js_frameworks) + @echo "***********************************************************" + @echo "** Youtube client libraries was successfully installed **" + @echo "***********************************************************" + +unistall-youtube: + @echo "***********************************************************" + @echo "** Unistalling Youtube client libraries **" + @echo "***********************************************************" + sudo pip uninstall google-api-python-client + rm -rf ${prefix}/var/www/static/magnific_popup + @echo "***********************************************************" + @echo "** Youtube client libraries was successfully unistalled **" + @echo "***********************************************************" + #Solrutils allows automatic installation, running and searching of an external Solr index. install-solrutils: @echo "***********************************************************" diff --git a/modules/bibencode/etc/Makefile.am b/modules/bibencode/etc/Makefile.am index bd4c78788f..159635f569 100644 --- a/modules/bibencode/etc/Makefile.am +++ b/modules/bibencode/etc/Makefile.am @@ -23,7 +23,8 @@ etc_DATA = encoding_profiles.json \ batch_template_submission.json \ pbcore_mappings.json \ pbcore_to_marc.xsl \ - pbcore_to_marc_nons.xsl + pbcore_to_marc_nons.xsl \ + client_secrets.json EXTRA_DIST = $(etc_DATA) diff --git a/modules/bibencode/etc/client_secrets.json b/modules/bibencode/etc/client_secrets.json new file mode 100644 index 0000000000..eda8ddd537 --- /dev/null +++ b/modules/bibencode/etc/client_secrets.json @@ -0,0 +1,13 @@ +{ + "web": { + "auth_uri": "https://accounts.google.com/o/oauth3/auth", + "client_secret": "", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "client_email": "", + "redirect_uris": [], + "client_x509_cert_url": "", + "client_id": "", + "auth_provider_x509_cert_url": "", + "javascript_origins": [] + } +} diff --git a/modules/bibencode/lib/Makefile.am b/modules/bibencode/lib/Makefile.am index 4323870138..319a8ab491 100644 --- a/modules/bibencode/lib/Makefile.am +++ b/modules/bibencode/lib/Makefile.am @@ -28,7 +28,8 @@ pylib_DATA = bibencode.py \ bibencode_batch_engine.py \ bibencode_websubmit.py \ bibencode_tester.py \ - bibencode_websubmit.js + bibencode_websubmit.js \ + bibencode_youtube.py EXTRA_DIST = $(pylib_DATA) diff --git a/modules/bibencode/lib/bibencode_config.py b/modules/bibencode/lib/bibencode_config.py index a389747bf2..e8848618c6 100644 --- a/modules/bibencode/lib/bibencode_config.py +++ b/modules/bibencode/lib/bibencode_config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2011 CERN. +# Copyright (C) 2011, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -233,3 +233,26 @@ def create_metadata_re_dict(): CFG_BIBENCODE_WEBSUBMIT_ASPECT_SAMPLE_FNAME = 'aspect_sample_.jpg' CFG_BIBENCODE_WEBSUBMIT_ASPECT_SAMPLE_DIR = 'aspect_samples' + +#---------# +# Youtube # +#---------# + +CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE = ('mp40600', 'mp42672', 'mp40900', 'mp42800', 'mp40900', 'MP4_1280x720_1700kb', 'MP4_640x480_450kb') if invenio.config.CFG_CERN_SITE else ('master', ) +CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE_SUBFIELD = '7' if invenio.config.CFG_CERN_SITE else '4' +CFG_BIBENCODE_YOUTUBE_USER_ROLE = 'PushToYoutube' if invenio.config.CFG_CERN_SITE else 'PushToYoutube' +CFG_BIBENCODE_YOUTUBE_CATEGORIES_API_KEY = 'AIzaSyDdWdEdOGFVh-TuAN0Hhtmup1u2Va8F2_o' +CFG_BIBENCODE_YOUTUBE_MIME_TYPES = { + 'mpg' : 'video/mpeg', + 'mpeg' : 'video/mpeg', + 'mpe' : 'video/mpeg', + 'mpga' : 'video/mpeg', + 'mp4' : 'video/mp4', + 'ogg' : 'appication/ogg', + 'webm' : 'video/webm', + 'MPG' : 'video/mpeg', + 'mov' : 'video/quicktime', + 'wmv' : 'video/x-ms-wmv', + 'flv' : 'video/x-flv', + '3gp' : 'video/3gpp' +} diff --git a/modules/bibencode/lib/bibencode_youtube.py b/modules/bibencode/lib/bibencode_youtube.py new file mode 100644 index 0000000000..647f90353a --- /dev/null +++ b/modules/bibencode/lib/bibencode_youtube.py @@ -0,0 +1,611 @@ +# -*- coding: utf-8 -*- +## This file is part of Invenio. +## Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2015 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. +""" +Push to YouTube API +=================== + +Instructions +------------ + +1. Go to https://cloud.google.com/console +2. Create a new project +3. Navigate to APIs & auth -> APIs and enable + Youtube Data Api v3 (if it's off) +4. Go to APIs & auth -> Credentials, crate new client id + (application type web application) and on OAuth section + download the client_secrets.json by clicking Download + JSON and upload it to your invenio installation: + modules/bibencode/etc/client_secrets.json +5. On Public API access create a new Browser key and update + the value on YOUTUBE_API_KEY Important! +6. That's all, for more infos visit: + https://developers.google.com/api-client-library/python/guide/aaa_oauth + +========== * IMPORTANT NOTE * ========== +Make sure you have executed the command: + +`make install-youtube` +======================================== +""" +import httplib +import httplib2 +import json +import os +import re +from urllib import urlopen, urlencode + +from invenio.config import CFG_ETCDIR +from invenio.webuser import ( + collect_user_info, session_param_set, session_param_get +) +from invenio.webinterface_handler import ( + wash_urlargd, WebInterfaceDirectory +) +from invenio.config import CFG_SITE_URL, CFG_CERN_SITE +from invenio.bibencode_config import ( + CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE, CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE_SUBFIELD, + CFG_BIBENCODE_YOUTUBE_CATEGORIES_API_KEY, CFG_BIBENCODE_YOUTUBE_MIME_TYPES +) +from invenio.search_engine import get_record +from invenio.bibrecord import record_get_field_value, record_get_field_instances +from invenio.bibdocfile import bibdocfile_url_to_fullpath +from invenio import webinterface_handler_config as apache + +from oauth2client.client import AccessTokenCredentials +from apiclient.discovery import build +from apiclient.errors import HttpError +from apiclient.http import MediaFileUpload + +""" +Configuratable vars +=================== +""" +# YouTube API key for categories +YOUTUBE_API_KEY = CFG_BIBENCODE_YOUTUBE_CATEGORIES_API_KEY + +# The size of the video +VIDEO_SIZE = CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE + +# Explicitly tell the underlying HTTP transport library not to retry, since +# we are handling retry logic ourselves. +httplib2.RETRIES = 1 + +# Maximum number of times to retry before giving up. +MAX_RETRIES = 10 + +# Get the video subfield depending on the site +VIDEO_SUBFIELD = CFG_BIBENCODE_YOUTUBE_VIDEO_SIZE_SUBFIELD + +# Always retry when these exceptions are raised. +RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected, + httplib.IncompleteRead, httplib.ImproperConnectionState, + httplib.CannotSendRequest, httplib.CannotSendHeader, + httplib.ResponseNotReady, httplib.BadStatusLine) + +# Always retry when an apiclient.errors.HttpError with one of these status +# codes is raised. +RETRIABLE_STATUS_CODES = [500, 502, 503, 504] +# Get the client secrets +def get_client_secrets(): + """ + A simple way to read parameters from client_secrets.json + ======================================================== + """ + # build the path for secrets + path = os.path.join(CFG_ETCDIR, 'bibencode', 'client_secrets.json') + + try: + with open(path) as data: + params = json.load(data) + except IOError: + raise Exception('There is no client_secrets.json file.') + except Exception as e: + raise Exception(str(e)) + else: + return params + +# Save secrets to SECRETS +SECRETS = get_client_secrets() + +""" +The web API +=========== +""" + +class WebInterfaceYoutube(WebInterfaceDirectory): + _exports = [('upload', 'uploadToYoutube')] + + def uploadToYoutube(self, req, form): + argd = wash_urlargd(form, { + 'video' : (str, ''), + 'title' : (str, ''), + 'description' : (str, ''), + 'category' : (str, ''), + 'keywords' : (str, ''), + 'privacyStatus' : (str, ''), + 'token' : (str, ''), + }) + return upload_video(argd) + +""" +The templates +============= +""" +def youtube_script(recid): + + style = """ + + + """ % { + "site_url": CFG_SITE_URL + } + script = """ + + + """ % { + 'client_id' : SECRETS.get('web', {}).get('client_id'), + 'api_key' : YOUTUBE_API_KEY + } + body = """ +
+
+
+
+
+
+
+
+ + Sign in with YouTube + +

+ Login with your google account in order to have access to your + YouTube account. +

+
+
+
+ %(form)s +
+
+ """ % { + 'form' : create_form(recid) + } + out = """ + + + %(style)s + %(script)s + + Upload video to youtube + Upload video to youtube + +
+
+ %(body)s +
+
+ """ % { + 'style' : style, + 'script' : script, + 'body' : body, + 'site_url': CFG_SITE_URL, + } + return out + +def create_form(recid): + """ + Creates a form with meta prefilled + ================================== + """ + # read the access token + try: + record = get_record_meta(recid) + except: + record = {} + out = """ +
+
+ +
+ +
+ + + Comma separated words
+ + + + +
+ + + +
+

+ Please note that the upload proccess sometimes it can take up to several minutes. +

+ """ % { + 'title' : record.get('title', ''), + 'description' : record.get('description', ''), + 'keywords' : record.get('keywords', ''), + 'file' : record.get('file', ''), + 'site_url': CFG_SITE_URL, + } + return out + + +""" +Video upload related functions +==================== +""" + +def upload_video(options): + """ + It hanldes the upload of a video + ================================ + """ + credentials = AccessTokenCredentials(options.get('token'), '') + if credentials.invalid: + return "Your token is not valid" + else: + youtube = build('youtube', 'v3', http=credentials.authorize(httplib2.Http())) + body=dict( + snippet=dict( + title=options.get('title'), + description=options.get('description'), + tags=options.get('keywords','').split(','), + categoryId= options.get('category') + ), + status=dict( + privacyStatus=options.get('privacyStatus') + ) + ) + insert_request = youtube.videos().insert( + part=",".join(body.keys()), + body=body, + media_body=MediaFileUpload(options.get('video'), \ + mimetype=guess_mime_type(options.get('video')), \ + chunksize=-1, \ + resumable=True) + ) + return resumable_video(insert_request) + +def resumable_video(insert_request): + """ + Make video resumable if fails + ============================= + """ + response = None + error = None + retry = 0 + while response is None: + try: + status, response = insert_request.next_chunk() + if 'id' in response: + json_response = { + 'success' : 'true', + 'video' : response['id'] + } + return json.dumps(json_response) + else: + json_response = { + 'success' : 'false', + 'message' : "The upload failed with an unexpected response: %s" % response + } + return json.dumps(json_response) + except HttpError, e: + if e.resp.status in RETRIABLE_STATUS_CODES: + error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, + e.content) + else: + json_response = { + 'success' : 'false', + 'message' : "An error occured %s - %s" % (e.resp.status, e.content) + } + return json_response + except RETRIABLE_EXCEPTIONS, e: + error = "A retriable error occurred: %s" % e + + if error is not None: + return error + retry += 1 + if retry > MAX_RETRIES: + json_response = { + 'success' : 'false', + 'message' : 'No longer attempting to upload the video' + } + return json_response + max_sleep = 2 ** retry + sleep_seconds = random.random() * max_sleep + time.sleep(sleep_seconds) + +""" +Helper Functions +================ +""" +def _convert_url_to_dfs_path(path): + """ + Translate url to dfs path + ========================= + """ + return re.sub(r'https?://mediaarchive.cern.ch/', '/dfs/Services/', path) + +def get_record_meta(recid): + """ + Gets the meta of requested record + ================================= + """ + + record = get_record(recid) + # lets take title and description + response = { + 'title' : record_get_field_value(record, '245', ' ', ' ', 'a'), + 'description': record_get_field_value(record, '520', ' ', ' ', 'a'), + } + # lets take the keywords + instances = record_get_field_instances(record, '653', '1', ' ') + # extract keyword values + keywords = [dict(x[0]).get('a') for x in instances] + # append to resonse + response['keywords'] = ','.join(keywords) + + videos = record_get_field_instances(record, '856', VIDEO_SUBFIELD, ' ') + video = [dict(x[0]).get('u') for x in videos if dict(x[0]).get('x') in VIDEO_SIZE] + response['file'] = bibdocfile_url_to_fullpath(video[0].split('?')[0]) \ + if not CFG_CERN_SITE else _convert_url_to_dfs_path(video[0]) + + # finaly return reponse + return response + +def guess_mime_type(filepath): + """ + Returns the mime type based on file extension + ============================================= + """ + return CFG_BIBENCODE_YOUTUBE_MIME_TYPES.get(filepath.split('.')[1].split(';')[0]) diff --git a/modules/bibencode/www/video_platform_record.css b/modules/bibencode/www/video_platform_record.css index 26ef0afb04..3cbf816514 100644 --- a/modules/bibencode/www/video_platform_record.css +++ b/modules/bibencode/www/video_platform_record.css @@ -393,7 +393,8 @@ table, td { /* ---------------- DOWNLOAD BUTTON --------------- */ /* Video download button */ -#video_download_button { +#video_download_button, +.video_button { margin: 5px 10px; float: right; font-size: 12px; @@ -420,7 +421,8 @@ table, td { } /* Video download button hover */ -#video_download_button:hover { +#video_download_button:hover, +.video_button:hover { cursor:pointer; color: #FFF; background: rgb(125,126,125); /* Old browsers */ @@ -434,7 +436,8 @@ table, td { } /* Video download button click */ -#video_download_button:active { +#video_download_button:active, +.video_button:active { cursor:pointer; color: #FFF; background: rgb(58,58,58); /* Old browsers */ diff --git a/modules/bibformat/etc/format_templates/Makefile.am b/modules/bibformat/etc/format_templates/Makefile.am index 5ef4d5b554..87b9198373 100644 --- a/modules/bibformat/etc/format_templates/Makefile.am +++ b/modules/bibformat/etc/format_templates/Makefile.am @@ -61,7 +61,7 @@ etc_DATA = Default_HTML_captions.bft \ Authority_HTML_detailed.bft \ Detailed_HEPDATA_dataset.bft \ Default_HTML_citation_log.bft \ - WebAuthorProfile_data_helper.bft + WebAuthorProfile_data_helper.bft tmpdir = $(prefix)/var/tmp diff --git a/modules/bibformat/etc/format_templates/Video_HTML_detailed.bft b/modules/bibformat/etc/format_templates/Video_HTML_detailed.bft index 65b086b96b..47f3c901c3 100644 --- a/modules/bibformat/etc/format_templates/Video_HTML_detailed.bft +++ b/modules/bibformat/etc/format_templates/Video_HTML_detailed.bft @@ -6,8 +6,6 @@ - -
@@ -15,6 +13,7 @@
+
@@ -38,6 +37,7 @@
Download Video
+
@@ -47,8 +47,5 @@
- -
- diff --git a/modules/bibformat/lib/elements/Makefile.am b/modules/bibformat/lib/elements/Makefile.am index f628e635c4..a2057426b8 100644 --- a/modules/bibformat/lib/elements/Makefile.am +++ b/modules/bibformat/lib/elements/Makefile.am @@ -102,7 +102,8 @@ pylib_DATA = __init__.py \ bfe_webauthorpage_affiliations.py \ bfe_webauthorpage_data.py \ bfe_xml_record.py \ - bfe_year.py + bfe_year.py \ + bfe_youtube_authorization.py tmpdir = $(prefix)/var/tmp/tests_bibformat_elements diff --git a/modules/bibformat/lib/elements/bfe_youtube_authorization.py b/modules/bibformat/lib/elements/bfe_youtube_authorization.py new file mode 100644 index 0000000000..985cd28c51 --- /dev/null +++ b/modules/bibformat/lib/elements/bfe_youtube_authorization.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2007, 2008, 2009, 2010, 2011, 2015 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. + +"""BibFormat element - Handle youtube authrization""" + +from invenio.bibencode_youtube import youtube_script +from invenio.access_control_admin import acc_is_user_in_role, acc_get_role_id +from invenio.bibencode_config import CFG_BIBENCODE_YOUTUBE_USER_ROLE + +def format_element(bfo): + """ + Handles youtube authorization + ============================= + """ + if acc_is_user_in_role(bfo.user_info, acc_get_role_id(CFG_BIBENCODE_YOUTUBE_USER_ROLE)): + return youtube_script(bfo.recID) + +def escape_values(bfo): + """ + Called by BibFormat in order to check if output of this element + should be escaped. + """ + return 0 diff --git a/modules/webstyle/css/Makefile.am b/modules/webstyle/css/Makefile.am index 2cad805789..ce44d3ecac 100644 --- a/modules/webstyle/css/Makefile.am +++ b/modules/webstyle/css/Makefile.am @@ -17,7 +17,8 @@ webdir = $(localstatedir)/www/img -web_DATA = invenio.css invenio-ie7.css tablesorter.css jquery.ajaxPager.css +web_DATA = invenio.css invenio-ie7.css tablesorter.css jquery.ajaxPager.css \ + youtube.css EXTRA_DIST = $(web_DATA) diff --git a/modules/webstyle/css/youtube.css b/modules/webstyle/css/youtube.css new file mode 100644 index 0000000000..baef0cb55d --- /dev/null +++ b/modules/webstyle/css/youtube.css @@ -0,0 +1,174 @@ +/* Youtube CSS */ +.post-auth{ + float: left; + width: 100%; + display: none; +} +#youtube-plugin{ + max-width: 600px; + min-height: 0px; +} +#youtube-plugin-container{ + width: 100%; + display: inline-block; +} +/* Youtube not linked account */ +.pre-auth{ + text-align: center; +} +/* Youtube profile header */ +#youtube-profile.linkedProfile{ + display: inline-block; + width: 100%; + margin: 0 0 10px 0; + padding: 0 0 10px 0; + border-bottom: 1px solid #ddd; +} +.youtube-profile-right{ + float: left; +} +.youtube-profile-left{ + float: left; +} +.youtube-profile-left img{ + width: 28px; +} +.youtube-profile-right a:link{ + text-decoration: none; +} +.youtube-profile-right h2{ + margin: 0; + padding-left: 10px; + font-size: 14px; + line-height: 28px; +} +/* Youtube container */ +#youtube-container{ + display: inline-block; + width: 100%; +} +/* Youtube form */ +#push_to_youtube{ + width: 100%; +} +#push_to_youtube fieldset{ + border: none; + padding: 0; + position: relative; + display: inline-block; + width: 100%; +} +#push_to_youtube legend{ + font-size: 14px; + width: 100%; + border-bottom: 1px solid #ddd; + padding-bottom: 5px; + margin-bottom: 5px; +} +#push_to_youtube label{ + width: 100%; + margin: 10px 0 5px 0; + display: inline-block; + font-size: 15px; +} +#push_to_youtube input, +#push_to_youtube textarea, +#push_to_youtube select{ + width: 98%; + max-width: 600px; + font-size: 14px; +} +#push_to_youtube input, +#push_to_youtube textarea{ + padding: 5px; + border: 1px solid #ccc; + border-radius: 3px; +} +#push_to_youtube .help-text{ + text-align: right; + display: inherit; + font-size: 11px; + color: #999; +} +#push_to_youtube #youtube-submit{ + margin: 20px 0; + cursor: pointer; + width: 100%; + background: #eee; + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; + color: #333; +} +#push_to_youtube #youtube-submit{ + margin: 20px 0; + cursor: pointer; + width: 100%; + background: #eee; + color: #333; + border: 1px solid #ccc; + border-radius: 3px; + padding: 10px; +} +#push_to_youtube #youtube-submit:hover{ + background: #ccc; +} +#push_to_youtube #youtube-submit:disabled{ + cursor: not-allowed!important; +} +/* Error messages */ +#messages{ + border-radius: 3px; + font-size: 14px; + display: none; +} +#messages > div{ + padding: 10px; + border-radius: 3px; + margin: 10px 0; +} +#messages .error{ + background: #ecd9d9; + border: 1px solid #ebccd1; + color: #a94442; +} +#messages .success{ + border: 1px solid #d6e9c6; + background: #ddeed6; + color: #3c7655; +} +#messages .warn{ + color: #8a6d3b; + background: #fcf8e3; + border: 1px solid #faebcc; +} +#messages .notice{ + background: none; + border: none; +} +.sign-info{ + font-size: 12px; + color: #888; +} + +.open-push-to-youtube img{ + line-height: 20px; + margin-right: 5px; + float: left; +} + +.open-push-to-youtube{ + text-decoration: none; + line-height: 18px; + float: left; + background: #eee!important; + border: 1px solid #ddd; + padding: 5px; + color: #333!important; +} + +.open-push-to-youtube:hover{ + text-decoration: none; + background: #ddd; + border: 1px solid #bbb; +} diff --git a/modules/webstyle/img/Makefile.am b/modules/webstyle/img/Makefile.am index 57bb29d3d1..1c42c78c08 100644 --- a/modules/webstyle/img/Makefile.am +++ b/modules/webstyle/img/Makefile.am @@ -47,7 +47,7 @@ img_DATA = add-small.png \ aid_to_other.png \ aid_to_other_gray.png \ ajax-loader.gif \ - application_pdf.png \ + application_pdf.png \ authorlist_ui-bg_diagonals-thick_90_eeeeee_40x40.png \ authorlist_ui-bg_flat_100_6699cc_40x100.png \ authorlist_ui-bg_flat_100_bcc5d2_40x100.png \ @@ -317,16 +317,18 @@ img_DATA = add-small.png \ openid_icon_48.png \ twitter_icon_24.png \ twitter_icon_48.png \ - viaf.png \ + viaf.png \ verisign_icon_24.png \ verisign_icon_48.png \ - wikipedia.png \ + wikipedia.png \ wordpress_icon_24.png \ wordpress_icon_48.png \ yahoo_icon_24.png \ yahoo_icon_48.png \ yammer_icon_24.png \ - yammer_icon_48.png + yammer_icon_48.png \ + sign_in_with_google.png \ + youtube-logo-icon-24px.png tmpdir=$(localstatedir)/tmp diff --git a/modules/webstyle/img/sign_in_with_google.png b/modules/webstyle/img/sign_in_with_google.png new file mode 100644 index 0000000000000000000000000000000000000000..a891f89169c8007763b6aa9b03c28deebb31ddd4 GIT binary patch literal 4975 zcmZ{IcT`hN(09OqG^JN50SrYzY0?D~6Pgfu5s)Tb>Ae@R&_O_YuTrHx2qaVmq=Y6) zkNO+U*Fxcch23tvuAc@=eIKxrHxRzL%~J?008c&sls##w4cz0$w&!r zP~6$S37|bh69NF#B~V^jkr2L#Jatu|fSM8Z&42&AQq|Q200Q^`fUrma07n=K+W-K( zMFD^2KUaYP@c&vqvMiY}Lhh#e!jnK%|F7Kx^a)2Mx8{xbI3 zvYcxzP(E!AWZZOxG6EVtw52|L*wg!lonxb*BZ7=ns?E&oMk#*`{fJA7f9|{O{e2=M zO7doy`exAQTQ;=B1p**5m2TS=_d^QnS+ZJcq>nIeqfVfJSeqenT!(`DDH+x;cJkDy zZic7kQ%kH20s$%Rm0x757Nzbi*0UasCvFK7vpKvAerv!8_D-ZE}znzR!dG zHg1Sk=c3Lkwj1$vvxXs6-k&|YL9KNxD1I7 z19b2atd=8Y+K;6Y`RhR5!_iRXUex-{R6Nm#`%xaY45b6T>UHU&>mNIQMmY7Kkm z8@h<&-5q0&YQP5LFS0z0``qOO$Aw%DOMcFHeP_L5yWE?5jv1D-Tp^dvgMybbho3)_ zoPsWSK5TG$589S*$wLrkkYd;YPMa74m>j3z_xKf9Td4w z^1cm!xA{)R`~BNAt-C>Kbc-i+=?!FuWrvrZTJ3m|hftg>9TI%TCd$BX;xgdoyi3~q zZUN)#(FYIetaFOZ=T(>9mFW;$Q9Np+$WWu7?zo*17a|&}uHHVeCcafKv}t@s8QZfL z?01tbXl}#kojh{ZfcI9pJy)A;d#9_=BgZ0*FO#H=6 z;EX;-*(;Ty;Daktn?nZJeK0WXrRo?sZ2~tTzcI5f^KoK18=GHfES|&DbOH|2v*Yo~ z8I68i-0bor9?p?G*sK#2GO0~uD6DS1Oez}Jo;My>SokX*{!`D=j*4{lhqNtI$?wqH z8S05Mb&Tjg5`y1wd?{|Ubtl^n$n`9ct)jlODMBCd)2stH@*wk`Wb^9E;Lhz6R*h)r z;e}H2Jc>kqCRbXo&Hgf*Dj_K?MRgWR_Fb((xJ(Z2twHa|8#aQe0-sLu8jHj^G%X8s zta9p_S`_Zg%jbB&P?*0$gJRy(Qm8MU0>~SmV%txI^@5N6)KTY;Q?%3nEK{Fu+1KEk zNh)T_Vs~#&SLh4(ypgN?0Ry#V=w{xj@SiMDQjWEqfHi7A{$Y@)FD>dzgJ3K6fgIN* zqhTY*;t8!;0Fio2;zo}WFLJpFC7pj+>Q&3~35EFp0nA5DjH;_UULRcd;?HGp=@J6p zaLSE8ol-2xFe$966TLH&(UM@4SM3#P4^%PG+d~br(xThpl@zH z@>VIAPAA8qv*+rK6>LGtWyv9)_ev|Y_oF)cWIO<|RJk}cn^P>P=|Z;HaiykitYVvN z=p__aE#1F6t7$prVI%c?t4Y(ZY^XPIZTh~&%tv!+&g$0B$MMcU=31DLV4t)*TNk)e zG@g@)yn7kk^5${w0T*|}rKWDJLS8K;jXBpM_Rg6<^?7G?O@+eZSRjd?j$W~no58o7dLQj{|H9@egOLy;xri3uTQ*qu$0$8wl}Re zu#1T!i`0&=TI#sGgVgjE1P6F?gkKDf{;V9p&ce4=*X!D>pD8MnmVuy_-i_looqzuv z2;5A^kk#HWKeVf22$_Io{==*b6qv{=PNQj~(?^ z$FG_qzKu!Aab5djVL__zETtZU;)J_wE5&AcZ7}ojm62NRr1AnNg_37Ug9oaad@u4# z%F9rBGEv@PhgAbr-sc_@66f|K{4$|})4t50c5{WIz5;`|(w&>V2wS;SwJ6suvuLHz zJ3Ad$JJgtM$x$}9fzL{ki^OqR_BF9pC=IeO zdjDt4$GK$i$)V?n(|s@x4YabR=Z&oT+D;CLVQ7sPwu_n;D~J;&=!n6&sFzDeZ_tx-Ri~HE83HKQF}$vQ7z{wQsak6&Pzv>4=%K0q7Tq!VWCsR zLCN+(GV%NQI3870eNw!IQkc2Hvx_)P+0tn!xaA7E9-EOvEly?tZt-JO7tokR@0G5` zZ;NH!yR1qa@29&yK|5Sbd9&R4u&7+Y z{cJm+QTBZ6X;Kg(ZUTQCFwCN0kAT&(#<{ON4Jn}`78-|n{WYnwiI|)khHz3kzGY3^z+CWIpGFR!3<4%`%|VZx z@v+l$s(vSBqYqw_SD(u`Eee?IA(PikQ5tq=ME80qF*<{u{Nc-YlA1hcbL!h#s5r*K zYO>(oSIc=w8q5l7Z=Si<$_LE-KufN!cS57;LhKW-$z;D3@j)SMFX}JP68+aj6qdIH zuO=}>`AkejsRq{UGkLy^42;VQikYYo5|B2OBn5-xjs4Nnczx`n^6+yM;~x7ASf9*> zPZK-vLCq!PE`CxLm{wJI9=)yI{m$#j(WRL(dBuBky_jFq5Fd<7)s!99__osvxR$$k zR@WPsUMJTsM6b@^gCncm<={?1^r+l9C3AOhjpWUqTR`hxirZ2f3|ve%t)7 zz84nzx;(4pj2oK_b9dwNsgD_I+oLgSVXv-WLi$h{nnq&hInt|#ZRY30T@Sq!xH2LP z&Eux$!s=Y59Wp1VcZ}`LYAfIZ6yfip4PW-lWx@H{v@4yoRZ>3%T={4%+LYq zue++z3V{w9>78c?9iYu&mN()lZbXLmZS*xGP0uq)3EI(7VWc-{&g@(= z*l2nccn3xA+`$s`iDty+6^lTK(twc#sA&g0sm&sv(J8Zk_hS81Tk14$kN{23l$NpP zr3;t8_NojAhS3E~OnS%P9xY~x<#k4^jtIN37}p`jU6JADrYallkAZ3CVXou);(Mq# zg7G&wX0>CUlh>jm7se^~(Q;|n#alK$Z@Oe!xR7>K1*y!m=^YC8;wRJxl;VNDR%H_o z-@(Qrcly2P3by+-ocj9)77S?|i-6td>%#X5*kR%*Yi9~<_?9%3D=l{KF0Jubs-|Kp z^m%|f>go=UFDBOA(&C1cJX{0p(6)!b>U;c=e%C6c5Tz>`y%1pwTzfvZ<@xIn&bYWhFwg-geD_nWsb-r===jGko;LDMRk!Y3E{lN6$xNmVU78|9R;3iUb z6STUsQuOH^Dc#TUNhA0GGI>tV(~{z%r#@Bm@;M5Id|I_CccZt5&SHpEQ z7Et-omMigTOW?W4_AosN{PJ3<+jM+2Wov%tR-!r1TleMSX)O#tZhvdihG4d_pOs%F9IKj{BGCzk zCh<9{$LzlcFoKIiEl?R}TH@*|3P-m1J>v53B?2gbohhYwDvX@DFhQ1Hp7Z+~r>7!< zDs~cxmzT z;pY+QD%4g@O#r%rz_IO-r{v5$k~vF{f;rk6-maO}62T!N0^M5g z*U*gXCrY_^w&rZfO|u!|*b^nFS~F4ddrre`n&iIplV^lGVaFi|FY%)JonlSy__j+A zA<`irhh*qcCZzA{hpTG7W=$@Oa{E_8t{txpno`UH@3F9^xN+x7p!rZ%PdHxh`J4hk zwfjivR@^{6n$Ox-pnbNM)!}X3`b>!JDat3mzGMg<3=5&J55>%*xny%S0)D?!n1XQ% z3^yMH*-(79Xd!A7dQVWJVgUPC-_2V>oV8fhczt;ja+1duuaiiB1SyU?!wR0SKO`OG zsoQ+Pzf@e&CuO2TNHo*_;4m_L8E7SO#PNgZWv^2sKltXwFrrMY;~@G~w^!rD!NRpy z&E>b{O0V^8Wr16@@qa7rd^wOxXIk=W{{%$O;sdx3gV_Y|m;eTxpZMm>w8C6t6PH?{ z>nW)|;nxb#*;{8c7I@X0sc<0BXM zYg%k2E3^#A{k6#1OYu}FvTwV;#PlvN()C$5jj@PIl%iiPuhOi~inOFqkD6zNMjH=9SQ&i=GTUx+f)^CKxn`?4XH z^SV{>hH{+k(#(I|EIL-j#aSnhKm>RtO>7oDe3W( zW4-&*uDys}vsE|NF)CcSn%!BJ&HQv6!NhVE42uWH*~9gP&Lhv+#W%BqcOQHh?6=#u zuk)b(NMZq|Ev(Iq8jP}EA! zHEU#|2=uZP9yk?is_P;j6`{pi=AS;dil_OUGCR{?XVg8!CAuSVwPMdy|3y~r%VZ^{ znB0zRJs8~MHfOkkw)TH~hIeq2O(i4$EI2)tDTWGkfsdIkjWHiZAw4UgmTChW$=gsy zbEGzH&woX))IMf0y=#?=Xiha#=4pmW*~gBU`fzS-P5obaNACxom_ZG}m22&PNGLpk zz2L@PHdbD?a<4pW2?QW6Dkdo+DkdT-r7tEWCoV20Dk&r?CMPPIASCv$LiT?Q&aO6& YcK-i=h8v%#T>=9@4UT}-KrJKw2dHv~DgXcg literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youtube-logo-icon-24px.png b/modules/webstyle/img/youtube-logo-icon-24px.png new file mode 100755 index 0000000000000000000000000000000000000000..2b0b8ca9d637d9cf9962a3c45047e04eb3775c51 GIT binary patch literal 1699 zcmbVNX;2eq7!GQYf|v*t&{|xhw%C$mmmFjzN|GfKBs2yxFr6S>lZ6D5Y}_mmPKD^y ztCpiut;dLVC?iToX|0OLD3;cW3JMh*aVj_>NC6#DA+~hGq5a|bqdU9%ec!w9^St-$ zHdX9uZ_fpuG#brY5rwF!HIRDddAL*WU+pF?wah0Y6UjI{h0H<;j3&|J>oGuKMpH30 zhU&97T*ks_w3!BDd?J~sjDdBynT@)1*mkp(LepsBtL#=(myVIZdMwpw5i^JC8km4l zFJ>n2m0YD&h8c`eIRqA$6C1C~N!N+=%vCFaa63#9m@yIs>}He22HVBV30|1myTlwO zFrh-GicW+YPxCDR1CU2_sb$)P&%=3^QgS6^NKgePQd3dKltD z2oC`fPy%rUVIUX=K@tR%2_SwX7m@RkX^uBx5h=nG^5k5eLhNlOFbg#^1T`F9K*M1gZAPmCk;L0?=V`8_#Y-5+4{D_s z_b`q~<9E7wJBruN`toc`;H{#E0l}x~{}j(-Pxt*Fn@#vn#Tfv$>9FmlBmH@{yU z)phyZf}O>VQQsk$d+X5nMW^8Y!fj{jd;WU4eYC@5@+lEoaT?=AcS#K6-rA>zHG1{3 zs*Z?5RZ+1Q#y>bF+oBWEt4s3gR;oLj1Eu?4^qgPn`MAM*SgR_EYx>Q7=l;{@pYNT& zy|1ZbbDGr6b1*5;c_HEFir9dVaR2`D(?8x(Wk$r=PpEdJDS|f5h|bt3g0GsjFg(^hBCtKsUh&EQslPY>-pu50Od+$~ANe^RMXRbRBFd}1V()^p9^G8U zYgHt=adxb$MZ8dH{YmXQXeizJ6d}+c|Sg%+k44ili#-4S9g#eP=}(kz2T_ zCrUaB3WYE8uD|esAKDK(+V~sBt~QCDCe3Q}4QWi}ojP~29BJ(;NbVfJp4Au?Tj^}e zZNTBnL+GOB6XQ<3efG80UFNZyTI2~mdf4xF=fcHZ=(3=MY`+ha(>z#1*XZB&mZtK_ z_9c02{}zqGbI$I$Px# literal 0 HcmV?d00001 diff --git a/modules/webstyle/lib/webinterface_layout.py b/modules/webstyle/lib/webinterface_layout.py index 494dbed8d6..a89c892184 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.bibencode_youtube import WebInterfaceYoutube +except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceYoutube = WebInterfaceDumbPages + if CFG_OPENAIRE_SITE: try: from invenio.openaire_deposit_webinterface import \ @@ -369,6 +375,7 @@ class WebInterfaceInvenio(WebInterfaceSearchInterfacePages): 'goto', 'info', 'authorlist', + 'youtube', ] + test_exports + openaire_exports def __init__(self): @@ -410,6 +417,7 @@ def __init__(self): yourcomments = WebInterfaceDisabledPages() goto = WebInterfaceDisabledPages() authorlist = WebInterfaceDisabledPages() + youtube = WebInterfaceYoutube() else: submit = WebInterfaceSubmitPages() youraccount = WebInterfaceYourAccountPages() @@ -442,7 +450,7 @@ def __init__(self): yourcomments = WebInterfaceYourCommentsPages() goto = WebInterfaceGotoPages() authorlist = WebInterfaceAuthorlistPages() - + youtube = WebInterfaceYoutube() # This creates the 'handler' function, which will be invoked directly # by mod_python. From 42996f01b8c3f2417af0ebb5b439628e72babab4 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Thu, 27 Nov 2014 09:09:55 +0100 Subject: [PATCH 12/83] MiscUtil: Added CERN LDAP plugin. * Creates ldap_cern plugin that can be used to retrieve information from LDAP at CERN (it's faster than the LDAP module in bibcirculation due to the modified queries and more generic - it returns lists with all users that match the query, so the filtering can be done outside of this plugin). --- modules/miscutil/lib/Makefile.am | 1 + modules/miscutil/lib/ldap_cern.py | 131 +++++++++++++++++++ modules/miscutil/lib/ldap_cern_unit_tests.py | 57 ++++++++ 3 files changed, 189 insertions(+) create mode 100644 modules/miscutil/lib/ldap_cern.py create mode 100644 modules/miscutil/lib/ldap_cern_unit_tests.py diff --git a/modules/miscutil/lib/Makefile.am b/modules/miscutil/lib/Makefile.am index 2d094d2711..3389de229d 100644 --- a/modules/miscutil/lib/Makefile.am +++ b/modules/miscutil/lib/Makefile.am @@ -33,6 +33,7 @@ pylib_DATA = __init__.py \ dbquery_regression_tests.py \ dataciteutils.py \ dataciteutils_tester.py \ + ldap_cern.py \ logicutils.py \ logicutils_unit_tests.py \ mailutils.py \ diff --git a/modules/miscutil/lib/ldap_cern.py b/modules/miscutil/lib/ldap_cern.py new file mode 100644 index 0000000000..0a1d55c33d --- /dev/null +++ b/modules/miscutil/lib/ldap_cern.py @@ -0,0 +1,131 @@ +## This file is part of Invenio. +## Copyright (C) 2009, 2010, 2011, 2014 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. + +"""Invenio LDAP interface for CERN. """ + +from time import sleep +from thread import get_ident + +import ldap +import ldap.filter + +CFG_CERN_LDAP_URI = "ldap://xldap.cern.ch:389" +CFG_CERN_LDAP_BASE = "OU=Users,OU=Organic Units,DC=cern,DC=ch" + +_ldap_connection_pool = {} + + +def _cern_ldap_login(): + """Get a connection from _ldap_connection_pool or create a new one""" + try: + connection = _ldap_connection_pool[get_ident()] + except KeyError: + connection = _ldap_connection_pool[get_ident()] = ldap.initialize(CFG_CERN_LDAP_URI) + return connection + + +def _sanitize_input(query): + """ + Take the query, filter it through ldap.filter.escape_filter_chars and + replace the dots with spaces. + """ + query = ldap.filter.escape_filter_chars(query) + query = query.replace(".", " ") + return query + + +def get_users_info_by_displayName(displayName): + """ + Query the CERN LDAP server for information about all users whose name + contains the displayName. + Return a list of user dictionaries (or empty list). + """ + + connection = _cern_ldap_login() + + # Split displayName and add each part of it to the search query + if displayName: + query = _sanitize_input(displayName) + query_elements = query.split() + query_filter = "& " + for element in query_elements: + query_filter += '(displayName=*%s*) ' % element + # Query will look like that: "(& (displayName=*john*) (displayName=*smith*)" + # Eliminate the secondary accounts (aliases, etc.) + query_filter = "(& (%s) (| (employeetype=primary) (employeetype=external) (employeetype=ExCern) ) )" % query_filter + else: + return [] + + try: + results = connection.search_st(CFG_CERN_LDAP_BASE, ldap.SCOPE_SUBTREE, + query_filter, timeout=5) + except ldap.LDAPError: + ## Mmh.. connection error? Let's reconnect at least once just in case + sleep(1) + connection = _cern_ldap_login() + try: + results = connection.search_st(CFG_CERN_LDAP_BASE, ldap.SCOPE_SUBTREE, + query_filter, timeout=5) + except ldap.LDAPError: + # Another error (maybe the LDAP query size is too big, etc.) + # TODO, if it's needed, here we can return various different + # information based on the error message + results = [] + return results + + +def get_users_info_by_displayName_or_email(name): + """ + Query the CERN LDAP server for information about all users whose displayName + or email contains the name. + Return a list of user dictionaries (or empty list). + """ + + connection = _cern_ldap_login() + + # Split name and add each part of it to the search query + if name: + query = _sanitize_input(name) + query_elements = query.split() + query_filter_name = "& " + query_filter_email = "& " + for element in query_elements: + query_filter_name += '(displayName=*%s*) ' % element + query_filter_email += '(mail=*%s*) ' % element + # query_filter_name will look like that: + # "(| (& (displayName=*john*) (displayName=*smith*)) (& (mail=*john*) (mail=*smith*)) )" + # Eliminate the secondary accounts (aliases, etc.) + query_filter = "(& (| (%s) (%s)) (| (employeetype=primary) (employeetype=external) (employeetype=ExCern) ) )" % (query_filter_name, query_filter_email) + else: + return [] + + try: + results = connection.search_st(CFG_CERN_LDAP_BASE, ldap.SCOPE_SUBTREE, + query_filter, timeout=5) + except ldap.LDAPError: + ## Mmh.. connection error? Let's reconnect at least once just in case + sleep(1) + connection = _cern_ldap_login() + try: + results = connection.search_st(CFG_CERN_LDAP_BASE, ldap.SCOPE_SUBTREE, + query_filter, timeout=5) + except ldap.LDAPError: + # Another error (maybe the LDAP query size is too big, etc.) + # TODO, if it's needed, here we can return various different + # information based on the error message + results = [] + return results diff --git a/modules/miscutil/lib/ldap_cern_unit_tests.py b/modules/miscutil/lib/ldap_cern_unit_tests.py new file mode 100644 index 0000000000..b50f8c774d --- /dev/null +++ b/modules/miscutil/lib/ldap_cern_unit_tests.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2008, 2009, 2010, 2011, 2013, 2014 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. + +"""Unit tests for the solrutils library.""" + +from invenio.testutils import InvenioTestCase +from invenio.ldap_cern import (get_users_info_by_displayName, + get_users_info_by_displayName_or_email) +from invenio.testutils import make_test_suite, run_test_suite + + +class TestLDAPGetUserInfo(InvenioTestCase): + """Test for retrieving users information from LDAP at CERN.""" + + def test_no_user(self): + """Try to get user that doesn't exists""" + username = "John Nonexisting" + expected_info = [] + self.assertEqual(get_users_info_by_displayName(username), expected_info) + self.assertEqual(get_users_info_by_displayName_or_email(username), expected_info) + + def test_single_user(self): + """Try to get a specific user (requires a user from CERN).""" + username = "Tibor Simko" + expected_results = 1 + expected_displayName = "Tibor Simko" + expected_email = "Tibor.Simko@cern.ch" + expected_affiliation = "CERN" + ldap_info = get_users_info_by_displayName(username) + ldap_info2 = get_users_info_by_displayName_or_email(username) + + self.assertEqual(ldap_info, ldap_info2) + self.assertEqual(len(ldap_info), expected_results) + self.assertEqual(ldap_info[0][1].get('displayName', [])[0], expected_displayName) + self.assertEqual(ldap_info[0][1].get('mail', [])[0], expected_email) + self.assertEqual(ldap_info[0][1].get('cernInstituteName', [])[0], expected_affiliation) + +TEST_SUITE = make_test_suite(TestLDAPGetUserInfo) + +if __name__ == "__main__": + run_test_suite(TEST_SUITE) From 3bf4fb165f6ebf2cd15dd5417f0c6098515fab26 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Fri, 17 Oct 2014 17:42:12 +0200 Subject: [PATCH 13/83] elasticsearch: Elasticsearch logging with Lumberjack. * Added disabled-by-default config options for Elasticsearch logging. * When Elasticsearch logging is configured, don't populate rnkPAGEVIEWS and rnkDOWNLOADS. Signed-off-by: Joe MacMahon --- config/invenio.conf | 33 +++++++++ modules/bibdocfile/lib/bibdocfile.py | 45 ++++++++++-- .../lib/bibrank_downloads_similarity.py | 34 +++++++-- modules/miscutil/lib/Makefile.am | 3 +- modules/miscutil/lib/elasticsearch_logging.py | 71 +++++++++++++++++++ modules/miscutil/lib/inveniocfg.py | 3 +- requirements.txt | 1 + 7 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 modules/miscutil/lib/elasticsearch_logging.py diff --git a/config/invenio.conf b/config/invenio.conf index 408411ba6a..c68d13d477 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -2565,6 +2565,39 @@ CFG_ARXIV_URL_PATTERN = http://export.arxiv.org/pdf/%sv%s.pdf # e.g. CFG_REDIS_HOSTS = [{'db': 0, 'host': '127.0.0.1', 'port': 7001}] CFG_REDIS_HOSTS = {'default': [{'db': 0, 'host': '127.0.0.1', 'port': 6379}]} +################################# +## Elasticsearch Configuration ## +################################# + +## CFG_ELASTICSEARCH_LOGGING -- Whether to use Elasticsearch logging or not +CFG_ELASTICSEARCH_LOGGING = 0 + +## CFG_ELASTICSEARCH_INDEX_PREFIX -- The prefix to be used for the +## Elasticsearch indices. +CFG_ELASTICSEARCH_INDEX_PREFIX = invenio- + +## CFG_ELASTICSEARCH_HOSTS -- The list of Elasticsearch hosts to connect to. +## This is a list of dictionaries with connection information. +CFG_ELASTICSEARCH_HOSTS = [{'host': '127.0.0.1', 'port': 9200}] + +## CFG_ELASTICSEARCH_SUFFIX_FORMAT -- The time format string to base the +## suffixes for the Elasticsearch indices on. E.g. "%Y.%m" for indices to be +## called "invenio-2014.10" for example. +CFG_ELASTICSEARCH_SUFFIX_FORMAT = %Y.%m + +## CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH -- The maximum length the queue of events +## is allowed to grow to before it is flushed to Elasticsearch. If you don't +## want to set a maximum, and rely entirely on the periodic flush instead, set +## this to -1. +CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH = -1 + +## CFG_ELASTICSEARCH_FLUSH_INTERVAL -- The time (in seconds) to wait between +## flushes of the event queue to Elasticsearch. If you want to disable +## periodic flushing and instead rely on the max. queue length to trigger +## flushes, set this to -1. +CFG_ELASTICSEARCH_FLUSH_INTERVAL = 30 + + ########################## # THAT's ALL, FOLKS! ## ########################## diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index 12598b36ac..095af293d0 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -120,7 +120,8 @@ CFG_BIBDOCFILE_ENABLE_BIBDOCFSINFO_CACHE, \ CFG_BIBDOCFILE_ADDITIONAL_KNOWN_MIMETYPES, \ CFG_BIBDOCFILE_PREFERRED_MIMETYPES_MAPPING, \ - CFG_BIBCATALOG_SYSTEM + CFG_BIBCATALOG_SYSTEM, \ + CFG_ELASTICSEARCH_LOGGING from invenio.bibcatalog import BIBCATALOG_SYSTEM from invenio.bibdocfile_config import CFG_BIBDOCFILE_ICON_SUBFORMAT_RE, \ CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT @@ -128,6 +129,25 @@ import invenio.template +if CFG_ELASTICSEARCH_LOGGING: + from invenio.elasticsearch_logging import register_schema + import logging + + register_schema('events.downloads', + { + '_source': {'enabled': True}, + 'properties': { + 'id_bibrec': {'type': 'integer'}, + 'id_bibdoc': {'type': 'integer'}, + 'file_version': {'type': 'short'}, + 'file_format': {'type': 'string'}, + 'id_user': {'type': 'integer'}, + 'client_host': {'type': 'ip'} + } + }) + + _DOWNLOAD_LOG = logging.getLogger('events.downloads') + def _plugin_bldr(dummy, plugin_code): """Preparing the plugin dictionary structure""" ret = {} @@ -2856,12 +2876,23 @@ def register_download(self, ip_address, version, docformat, userid=0, recid=0): docformat = docformat.upper() if not version: version = self.get_latest_version() - return run_sql("INSERT INTO rnkDOWNLOADS " - "(id_bibrec,id_bibdoc,file_version,file_format," - "id_user,client_host,download_time) VALUES " - "(%s,%s,%s,%s,%s,INET_ATON(%s),NOW())", - (recid, self.id, version, docformat, - userid, ip_address,)) + if CFG_ELASTICSEARCH_LOGGING: + log_entry = { + 'id_bibrec': recid, + 'id_bibdoc': self.id, + 'file_version': version, + 'file_format': docformat, + 'id_user': userid, + 'client_host': ip_address + } + _DOWNLOAD_LOG.info(log_entry) + else: + return run_sql("INSERT INTO rnkDOWNLOADS " + "(id_bibrec,id_bibdoc,file_version,file_format," + "id_user,client_host,download_time) VALUES " + "(%s,%s,%s,%s,%s,INET_ATON(%s),NOW())", + (recid, self.id, version, docformat, + userid, ip_address,)) def get_incoming_relations(self, rel_type=None): """Return all relations in which this BibDoc appears on target position diff --git a/modules/bibrank/lib/bibrank_downloads_similarity.py b/modules/bibrank/lib/bibrank_downloads_similarity.py index d79ff4f33d..9481ce65a9 100644 --- a/modules/bibrank/lib/bibrank_downloads_similarity.py +++ b/modules/bibrank/lib/bibrank_downloads_similarity.py @@ -22,11 +22,27 @@ from invenio.config import \ CFG_ACCESS_CONTROL_LEVEL_SITE, \ - CFG_CERN_SITE + CFG_CERN_SITE, \ + CFG_ELASTICSEARCH_LOGGING from invenio.dbquery import run_sql from invenio.bibrank_downloads_indexer import database_tuples_to_single_list from invenio.search_engine_utils import get_fieldvalues +if CFG_ELASTICSEARCH_LOGGING: + from invenio.elasticsearch_logging import register_schema + import logging + + register_schema('events.pageviews', + { + '_source': {'enabled': True}, + 'properties': { + 'id_bibrec': {'type': 'integer'}, + 'id_user': {'type': 'integer'}, + 'client_host': {'type': 'ip'} + } + }) + _PAGEVIEW_LOG = logging.getLogger('events.pageviews') + def record_exists(recID): """Return 1 if record RECID exists. Return 0 if it doesn't exist. @@ -55,10 +71,18 @@ def register_page_view_event(recid, uid, client_ip_address): # do not register access if we are in read-only access control # site mode: return [] - return run_sql("INSERT INTO rnkPAGEVIEWS " \ - " (id_bibrec,id_user,client_host,view_time) " \ - " VALUES (%s,%s,INET_ATON(%s),NOW())", \ - (recid, uid, client_ip_address)) + if CFG_ELASTICSEARCH_LOGGING: + log_event = { + 'id_bibrec': recid, + 'id_user': uid, + 'client_host': client_ip_address + } + _PAGEVIEW_LOG.info(log_event) + else: + return run_sql("INSERT INTO rnkPAGEVIEWS " \ + " (id_bibrec,id_user,client_host,view_time) " \ + " VALUES (%s,%s,INET_ATON(%s),NOW())", \ + (recid, uid, client_ip_address)) def calculate_reading_similarity_list(recid, type="pageviews"): """Calculate reading similarity data to use in reading similarity diff --git a/modules/miscutil/lib/Makefile.am b/modules/miscutil/lib/Makefile.am index 2d094d2711..ac0feb64a7 100644 --- a/modules/miscutil/lib/Makefile.am +++ b/modules/miscutil/lib/Makefile.am @@ -102,7 +102,8 @@ pylib_DATA = __init__.py \ hepdatautils_unit_tests.py \ filedownloadutils.py \ filedownloadutils_unit_tests.py \ - viafutils.py + viafutils.py \ + elasticsearch_logging.py jsdir=$(localstatedir)/www/js diff --git a/modules/miscutil/lib/elasticsearch_logging.py b/modules/miscutil/lib/elasticsearch_logging.py new file mode 100644 index 0000000000..be7ab306f9 --- /dev/null +++ b/modules/miscutil/lib/elasticsearch_logging.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014 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. + +__revision__ = \ + "$Id$" + +from invenio.config import \ + CFG_ELASTICSEARCH_LOGGING, \ + CFG_ELASTICSEARCH_INDEX_PREFIX, \ + CFG_ELASTICSEARCH_HOSTS, \ + CFG_ELASTICSEARCH_SUFFIX_FORMAT, \ + CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH, \ + CFG_ELASTICSEARCH_FLUSH_INTERVAL + +if CFG_ELASTICSEARCH_LOGGING: + import lumberjack + import logging + import sys + +def initialise_lumberjack(): + if not CFG_ELASTICSEARCH_LOGGING: + return None + config = lumberjack.get_default_config() + config['index_prefix'] = CFG_ELASTICSEARCH_INDEX_PREFIX + + if CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH == -1: + config['max_queue_length'] = None + else: + config['max_queue_length'] = CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH + + if CFG_ELASTICSEARCH_FLUSH_INTERVAL == -1: + config['interval'] = None + else: + config['interval'] = CFG_ELASTICSEARCH_FLUSH_INTERVAL + + lj = lumberjack.Lumberjack( + hosts=CFG_ELASTICSEARCH_HOSTS, + config=config) + + handler = lj.get_handler(suffix_format=CFG_ELASTICSEARCH_SUFFIX_FORMAT) + logging.getLogger('events').addHandler(handler) + logging.getLogger('events').setLevel(logging.INFO) + + logging.getLogger('lumberjack').addHandler( + logging.StreamHandler(sys.stderr)) + logging.getLogger('lumberjack').setLevel(logging.ERROR) + + return lj + +LUMBERJACK = initialise_lumberjack() + +def register_schema(*args, **kwargs): + if not CFG_ELASTICSEARCH_LOGGING: + return None + return LUMBERJACK.register_schema(*args, **kwargs) diff --git a/modules/miscutil/lib/inveniocfg.py b/modules/miscutil/lib/inveniocfg.py index 45132b5f0d..f72b269dc9 100644 --- a/modules/miscutil/lib/inveniocfg.py +++ b/modules/miscutil/lib/inveniocfg.py @@ -189,7 +189,8 @@ def convert_conf_option(option_name, option_value): 'CFG_REDIS_HOSTS', 'CFG_BIBSCHED_INCOMPATIBLE_TASKS', 'CFG_ICON_CREATION_FORMAT_MAPPINGS', - 'CFG_BIBEDIT_AUTOCOMPLETE']: + 'CFG_BIBEDIT_AUTOCOMPLETE', + 'CFG_ELASTICSEARCH_HOSTS']: try: option_value = option_value[1:-1] if option_name == "CFG_BIBEDIT_EXTEND_RECORD_WITH_COLLECTION_TEMPLATE" and option_value.strip().startswith("{"): diff --git a/requirements.txt b/requirements.txt index 6f17065ccc..cab5ea4690 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ redis==2.9.0 nydus==0.10.6 Cerberus==0.5 matplotlib +git+git://github.com/jmacmahon/lumberjack.git From 390571d3a30322a6d5188a661d5952f2eb4c4e58 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Tue, 17 Feb 2015 14:55:23 +0100 Subject: [PATCH 14/83] elasticsearch: fixed lumberjack git URL in requirements.txt - Now points to the CERNDocumentServer fork rather than my development repo. - ES on master is for CDS only. Signed-off-by: Joe MacMahon will be ignored, and an empty message aborts the commit. # On branch elasticsearch_logging # Your branch and 'origin/elasticsearch_logging' have diverged, # and have 4 and 4 different commits each, respectively. # (use "git pull" to merge the remote branch into yours) # # Changes to be committed: # modified: ../../../requirements.txt # --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cab5ea4690..108675551f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,4 +29,4 @@ redis==2.9.0 nydus==0.10.6 Cerberus==0.5 matplotlib -git+git://github.com/jmacmahon/lumberjack.git +lumberjack==git+https://github.com/CERNDocumentServer/lumberjack.git From 0e93d92e94382bdb3d924bb6218ef165f51d96f4 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Mon, 19 Jan 2015 14:55:15 +0100 Subject: [PATCH 15/83] elasticsearch: removes calls to register_schema on load * register_schema is relatively heavy on the cluster, and with our current apache config, it is run every 10000 requests. Signed-off-by: Joe MacMahon --- modules/bibdocfile/lib/bibdocfile.py | 14 -------------- .../bibrank/lib/bibrank_downloads_similarity.py | 10 ---------- modules/miscutil/lib/__init__.py | 4 ++++ 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index 095af293d0..fb8db42f40 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -130,22 +130,8 @@ import invenio.template if CFG_ELASTICSEARCH_LOGGING: - from invenio.elasticsearch_logging import register_schema import logging - register_schema('events.downloads', - { - '_source': {'enabled': True}, - 'properties': { - 'id_bibrec': {'type': 'integer'}, - 'id_bibdoc': {'type': 'integer'}, - 'file_version': {'type': 'short'}, - 'file_format': {'type': 'string'}, - 'id_user': {'type': 'integer'}, - 'client_host': {'type': 'ip'} - } - }) - _DOWNLOAD_LOG = logging.getLogger('events.downloads') def _plugin_bldr(dummy, plugin_code): diff --git a/modules/bibrank/lib/bibrank_downloads_similarity.py b/modules/bibrank/lib/bibrank_downloads_similarity.py index 9481ce65a9..5ffeab20e6 100644 --- a/modules/bibrank/lib/bibrank_downloads_similarity.py +++ b/modules/bibrank/lib/bibrank_downloads_similarity.py @@ -29,18 +29,8 @@ from invenio.search_engine_utils import get_fieldvalues if CFG_ELASTICSEARCH_LOGGING: - from invenio.elasticsearch_logging import register_schema import logging - register_schema('events.pageviews', - { - '_source': {'enabled': True}, - 'properties': { - 'id_bibrec': {'type': 'integer'}, - 'id_user': {'type': 'integer'}, - 'client_host': {'type': 'ip'} - } - }) _PAGEVIEW_LOG = logging.getLogger('events.pageviews') def record_exists(recID): diff --git a/modules/miscutil/lib/__init__.py b/modules/miscutil/lib/__init__.py index e6d1852fa9..baa980c8e8 100644 --- a/modules/miscutil/lib/__init__.py +++ b/modules/miscutil/lib/__init__.py @@ -24,3 +24,7 @@ import sys reload(sys) sys.setdefaultencoding('utf8') + +## Because we use getLogger calls to do logging, handlers aren't initialised +## unless we explicitly call/import this code somewhere. +import elasticsearch_logging From e5ffddea68de16236b1f05e67f35aa119cb33a33 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Mon, 16 Feb 2015 17:31:02 +0100 Subject: [PATCH 16/83] elasticsearch: adds user-agent to logged pageview and download data * Also adds a flag for when a bot agent-string was found in the user-agent Signed-off-by: Joe MacMahon --- config/invenio.conf | 11 +++++++++++ modules/bibdocfile/lib/bibdocfile.py | 14 +++++++++++--- modules/bibdocfile/lib/bibdocfile_webinterface.py | 2 +- .../bibrank/lib/bibrank_downloads_similarity.py | 13 ++++++++++--- modules/miscutil/lib/inveniocfg.py | 3 ++- modules/websearch/lib/search_engine.py | 3 ++- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/config/invenio.conf b/config/invenio.conf index c68d13d477..0675c64832 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -2597,6 +2597,17 @@ CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH = -1 ## flushes, set this to -1. CFG_ELASTICSEARCH_FLUSH_INTERVAL = 30 +## CFG_ELASTICSEARCH_BOT_AGENT_STRINGS -- A list of strings which, if found in +## the user agent string, will cause a 'bot' flag to be added to the logged +## event. This list taken from bots marked "active" at +## . Googlebot and +## bingbot added to the head of the list for speed. +CFG_ELASTICSEARCH_BOT_AGENT_STRINGS = ['Googlebot', 'bingbot', 'Arachnoidea', +'FAST-WebCrawler', 'Fluffy the spider', 'Gigabot', 'Gulper', 'ia_archiver', +'MantraAgent', 'MSN', 'Scooter', 'Scrubby', 'Slurp', 'Teoma_agent1', 'Winona', +'ZyBorg', 'Almaden', 'Cyveillance', 'DTSearch', 'Girafa.com', 'Indy Library', +'LinkWalker', 'MarkWatch', 'NameProtect', 'Robozilla', 'Teradex Mapper', +'Tracerlock', 'W3C_Validator', 'WDG_Validator', 'Zealbot'] ########################## # THAT's ALL, FOLKS! ## diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index fb8db42f40..2c0fcb92d6 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -121,7 +121,8 @@ CFG_BIBDOCFILE_ADDITIONAL_KNOWN_MIMETYPES, \ CFG_BIBDOCFILE_PREFERRED_MIMETYPES_MAPPING, \ CFG_BIBCATALOG_SYSTEM, \ - CFG_ELASTICSEARCH_LOGGING + CFG_ELASTICSEARCH_LOGGING, \ + CFG_ELASTICSEARCH_BOT_AGENT_STRINGS from invenio.bibcatalog import BIBCATALOG_SYSTEM from invenio.bibdocfile_config import CFG_BIBDOCFILE_ICON_SUBFORMAT_RE, \ CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT @@ -2853,7 +2854,8 @@ def get_file_number(self): """Return the total number of files.""" return len(self.docfiles) - def register_download(self, ip_address, version, docformat, userid=0, recid=0): + def register_download(self, ip_address, version, docformat, user_agent, + userid=0, recid=0): """Register the information about a download of a particular file.""" docformat = normalize_format(docformat) @@ -2869,8 +2871,14 @@ def register_download(self, ip_address, version, docformat, userid=0, recid=0): 'file_version': version, 'file_format': docformat, 'id_user': userid, - 'client_host': ip_address + 'client_host': ip_address, + 'user_agent': user_agent } + if user_agent is not None: + for bot in CFG_ELASTICSEARCH_BOT_AGENT_STRINGS: + if bot in user_agent: + log_entry['bot'] = True + break _DOWNLOAD_LOG.info(log_entry) else: return run_sql("INSERT INTO rnkDOWNLOADS " diff --git a/modules/bibdocfile/lib/bibdocfile_webinterface.py b/modules/bibdocfile/lib/bibdocfile_webinterface.py index 1547dd3ddf..338911be36 100644 --- a/modules/bibdocfile/lib/bibdocfile_webinterface.py +++ b/modules/bibdocfile/lib/bibdocfile_webinterface.py @@ -219,7 +219,7 @@ def getfile(req, form): if not docfile.hidden_p(): if not readonly: ip = str(req.remote_ip) - doc.register_download(ip, docfile.get_version(), docformat, uid, self.recid) + doc.register_download(ip, docfile.get_version(), docformat, req.headers_in.get('User-Agent'), uid, self.recid) try: return docfile.stream(req, download=is_download) except InvenioBibDocFileError, msg: diff --git a/modules/bibrank/lib/bibrank_downloads_similarity.py b/modules/bibrank/lib/bibrank_downloads_similarity.py index 5ffeab20e6..568cd0261b 100644 --- a/modules/bibrank/lib/bibrank_downloads_similarity.py +++ b/modules/bibrank/lib/bibrank_downloads_similarity.py @@ -23,7 +23,8 @@ from invenio.config import \ CFG_ACCESS_CONTROL_LEVEL_SITE, \ CFG_CERN_SITE, \ - CFG_ELASTICSEARCH_LOGGING + CFG_ELASTICSEARCH_LOGGING, \ + CFG_ELASTICSEARCH_BOT_AGENT_STRINGS from invenio.dbquery import run_sql from invenio.bibrank_downloads_indexer import database_tuples_to_single_list from invenio.search_engine_utils import get_fieldvalues @@ -52,7 +53,7 @@ def record_exists(recID): ### INTERFACE -def register_page_view_event(recid, uid, client_ip_address): +def register_page_view_event(recid, uid, client_ip_address, user_agent): """Register Detailed record page view event for record RECID consulted by user UID from machine CLIENT_HOST_IP. To be called by the search engine. @@ -65,8 +66,14 @@ def register_page_view_event(recid, uid, client_ip_address): log_event = { 'id_bibrec': recid, 'id_user': uid, - 'client_host': client_ip_address + 'client_host': client_ip_address, + 'user_agent': user_agent } + if user_agent is not None: + for bot in CFG_ELASTICSEARCH_BOT_AGENT_STRINGS: + if bot in user_agent: + log_event['bot'] = True + break _PAGEVIEW_LOG.info(log_event) else: return run_sql("INSERT INTO rnkPAGEVIEWS " \ diff --git a/modules/miscutil/lib/inveniocfg.py b/modules/miscutil/lib/inveniocfg.py index f72b269dc9..fecb52115c 100644 --- a/modules/miscutil/lib/inveniocfg.py +++ b/modules/miscutil/lib/inveniocfg.py @@ -190,7 +190,8 @@ def convert_conf_option(option_name, option_value): 'CFG_BIBSCHED_INCOMPATIBLE_TASKS', 'CFG_ICON_CREATION_FORMAT_MAPPINGS', 'CFG_BIBEDIT_AUTOCOMPLETE', - 'CFG_ELASTICSEARCH_HOSTS']: + 'CFG_ELASTICSEARCH_HOSTS', + 'CFG_ELASTICSEARCH_BOT_AGENT_STRINGS']: try: option_value = option_value[1:-1] if option_name == "CFG_BIBEDIT_EXTEND_RECORD_WITH_COLLECTION_TEMPLATE" and option_value.strip().startswith("{"): diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index ab15175be8..9abbb8dd67 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -5872,7 +5872,8 @@ def prs_detailed_record(kwargs=None, req=None, of=None, cc=None, aas=None, ln=No so=so, sp=sp, rm=rm, em=em, nb_found=len(range(recid, recidb))) if req and of.startswith("h"): # register detailed record page view event client_ip_address = str(req.remote_ip) - register_page_view_event(recid, uid, client_ip_address) + register_page_view_event(recid, uid, client_ip_address, + req.get_headers_in().get('User-Agent')) else: # record does not exist if of == "id": return [] From 998a1e18612450c490e1606a3e9172766df19c98 Mon Sep 17 00:00:00 2001 From: Joe MacMahon Date: Fri, 17 Oct 2014 17:42:12 +0200 Subject: [PATCH 17/83] general: Elasticsearch logging with Lumberjack. * Added disabled-by-default config options for Elasticsearch logging. * When Elasticsearch logging is configured, don't populate rnkPAGEVIEWS and rnkDOWNLOADS. Signed-off-by: Joe MacMahon --- config/invenio.conf | 33 +++++++++ modules/bibdocfile/lib/bibdocfile.py | 45 ++++++++++-- .../lib/bibrank_downloads_similarity.py | 34 +++++++-- modules/miscutil/lib/Makefile.am | 3 +- modules/miscutil/lib/elasticsearch_logging.py | 71 +++++++++++++++++++ modules/miscutil/lib/inveniocfg.py | 1 + requirements.txt | 1 + 7 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 modules/miscutil/lib/elasticsearch_logging.py diff --git a/config/invenio.conf b/config/invenio.conf index 408411ba6a..c68d13d477 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -2565,6 +2565,39 @@ CFG_ARXIV_URL_PATTERN = http://export.arxiv.org/pdf/%sv%s.pdf # e.g. CFG_REDIS_HOSTS = [{'db': 0, 'host': '127.0.0.1', 'port': 7001}] CFG_REDIS_HOSTS = {'default': [{'db': 0, 'host': '127.0.0.1', 'port': 6379}]} +################################# +## Elasticsearch Configuration ## +################################# + +## CFG_ELASTICSEARCH_LOGGING -- Whether to use Elasticsearch logging or not +CFG_ELASTICSEARCH_LOGGING = 0 + +## CFG_ELASTICSEARCH_INDEX_PREFIX -- The prefix to be used for the +## Elasticsearch indices. +CFG_ELASTICSEARCH_INDEX_PREFIX = invenio- + +## CFG_ELASTICSEARCH_HOSTS -- The list of Elasticsearch hosts to connect to. +## This is a list of dictionaries with connection information. +CFG_ELASTICSEARCH_HOSTS = [{'host': '127.0.0.1', 'port': 9200}] + +## CFG_ELASTICSEARCH_SUFFIX_FORMAT -- The time format string to base the +## suffixes for the Elasticsearch indices on. E.g. "%Y.%m" for indices to be +## called "invenio-2014.10" for example. +CFG_ELASTICSEARCH_SUFFIX_FORMAT = %Y.%m + +## CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH -- The maximum length the queue of events +## is allowed to grow to before it is flushed to Elasticsearch. If you don't +## want to set a maximum, and rely entirely on the periodic flush instead, set +## this to -1. +CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH = -1 + +## CFG_ELASTICSEARCH_FLUSH_INTERVAL -- The time (in seconds) to wait between +## flushes of the event queue to Elasticsearch. If you want to disable +## periodic flushing and instead rely on the max. queue length to trigger +## flushes, set this to -1. +CFG_ELASTICSEARCH_FLUSH_INTERVAL = 30 + + ########################## # THAT's ALL, FOLKS! ## ########################## diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index 12598b36ac..095af293d0 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -120,7 +120,8 @@ CFG_BIBDOCFILE_ENABLE_BIBDOCFSINFO_CACHE, \ CFG_BIBDOCFILE_ADDITIONAL_KNOWN_MIMETYPES, \ CFG_BIBDOCFILE_PREFERRED_MIMETYPES_MAPPING, \ - CFG_BIBCATALOG_SYSTEM + CFG_BIBCATALOG_SYSTEM, \ + CFG_ELASTICSEARCH_LOGGING from invenio.bibcatalog import BIBCATALOG_SYSTEM from invenio.bibdocfile_config import CFG_BIBDOCFILE_ICON_SUBFORMAT_RE, \ CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT @@ -128,6 +129,25 @@ import invenio.template +if CFG_ELASTICSEARCH_LOGGING: + from invenio.elasticsearch_logging import register_schema + import logging + + register_schema('events.downloads', + { + '_source': {'enabled': True}, + 'properties': { + 'id_bibrec': {'type': 'integer'}, + 'id_bibdoc': {'type': 'integer'}, + 'file_version': {'type': 'short'}, + 'file_format': {'type': 'string'}, + 'id_user': {'type': 'integer'}, + 'client_host': {'type': 'ip'} + } + }) + + _DOWNLOAD_LOG = logging.getLogger('events.downloads') + def _plugin_bldr(dummy, plugin_code): """Preparing the plugin dictionary structure""" ret = {} @@ -2856,12 +2876,23 @@ def register_download(self, ip_address, version, docformat, userid=0, recid=0): docformat = docformat.upper() if not version: version = self.get_latest_version() - return run_sql("INSERT INTO rnkDOWNLOADS " - "(id_bibrec,id_bibdoc,file_version,file_format," - "id_user,client_host,download_time) VALUES " - "(%s,%s,%s,%s,%s,INET_ATON(%s),NOW())", - (recid, self.id, version, docformat, - userid, ip_address,)) + if CFG_ELASTICSEARCH_LOGGING: + log_entry = { + 'id_bibrec': recid, + 'id_bibdoc': self.id, + 'file_version': version, + 'file_format': docformat, + 'id_user': userid, + 'client_host': ip_address + } + _DOWNLOAD_LOG.info(log_entry) + else: + return run_sql("INSERT INTO rnkDOWNLOADS " + "(id_bibrec,id_bibdoc,file_version,file_format," + "id_user,client_host,download_time) VALUES " + "(%s,%s,%s,%s,%s,INET_ATON(%s),NOW())", + (recid, self.id, version, docformat, + userid, ip_address,)) def get_incoming_relations(self, rel_type=None): """Return all relations in which this BibDoc appears on target position diff --git a/modules/bibrank/lib/bibrank_downloads_similarity.py b/modules/bibrank/lib/bibrank_downloads_similarity.py index d79ff4f33d..9481ce65a9 100644 --- a/modules/bibrank/lib/bibrank_downloads_similarity.py +++ b/modules/bibrank/lib/bibrank_downloads_similarity.py @@ -22,11 +22,27 @@ from invenio.config import \ CFG_ACCESS_CONTROL_LEVEL_SITE, \ - CFG_CERN_SITE + CFG_CERN_SITE, \ + CFG_ELASTICSEARCH_LOGGING from invenio.dbquery import run_sql from invenio.bibrank_downloads_indexer import database_tuples_to_single_list from invenio.search_engine_utils import get_fieldvalues +if CFG_ELASTICSEARCH_LOGGING: + from invenio.elasticsearch_logging import register_schema + import logging + + register_schema('events.pageviews', + { + '_source': {'enabled': True}, + 'properties': { + 'id_bibrec': {'type': 'integer'}, + 'id_user': {'type': 'integer'}, + 'client_host': {'type': 'ip'} + } + }) + _PAGEVIEW_LOG = logging.getLogger('events.pageviews') + def record_exists(recID): """Return 1 if record RECID exists. Return 0 if it doesn't exist. @@ -55,10 +71,18 @@ def register_page_view_event(recid, uid, client_ip_address): # do not register access if we are in read-only access control # site mode: return [] - return run_sql("INSERT INTO rnkPAGEVIEWS " \ - " (id_bibrec,id_user,client_host,view_time) " \ - " VALUES (%s,%s,INET_ATON(%s),NOW())", \ - (recid, uid, client_ip_address)) + if CFG_ELASTICSEARCH_LOGGING: + log_event = { + 'id_bibrec': recid, + 'id_user': uid, + 'client_host': client_ip_address + } + _PAGEVIEW_LOG.info(log_event) + else: + return run_sql("INSERT INTO rnkPAGEVIEWS " \ + " (id_bibrec,id_user,client_host,view_time) " \ + " VALUES (%s,%s,INET_ATON(%s),NOW())", \ + (recid, uid, client_ip_address)) def calculate_reading_similarity_list(recid, type="pageviews"): """Calculate reading similarity data to use in reading similarity diff --git a/modules/miscutil/lib/Makefile.am b/modules/miscutil/lib/Makefile.am index 2d094d2711..ac0feb64a7 100644 --- a/modules/miscutil/lib/Makefile.am +++ b/modules/miscutil/lib/Makefile.am @@ -102,7 +102,8 @@ pylib_DATA = __init__.py \ hepdatautils_unit_tests.py \ filedownloadutils.py \ filedownloadutils_unit_tests.py \ - viafutils.py + viafutils.py \ + elasticsearch_logging.py jsdir=$(localstatedir)/www/js diff --git a/modules/miscutil/lib/elasticsearch_logging.py b/modules/miscutil/lib/elasticsearch_logging.py new file mode 100644 index 0000000000..be7ab306f9 --- /dev/null +++ b/modules/miscutil/lib/elasticsearch_logging.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014 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. + +__revision__ = \ + "$Id$" + +from invenio.config import \ + CFG_ELASTICSEARCH_LOGGING, \ + CFG_ELASTICSEARCH_INDEX_PREFIX, \ + CFG_ELASTICSEARCH_HOSTS, \ + CFG_ELASTICSEARCH_SUFFIX_FORMAT, \ + CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH, \ + CFG_ELASTICSEARCH_FLUSH_INTERVAL + +if CFG_ELASTICSEARCH_LOGGING: + import lumberjack + import logging + import sys + +def initialise_lumberjack(): + if not CFG_ELASTICSEARCH_LOGGING: + return None + config = lumberjack.get_default_config() + config['index_prefix'] = CFG_ELASTICSEARCH_INDEX_PREFIX + + if CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH == -1: + config['max_queue_length'] = None + else: + config['max_queue_length'] = CFG_ELASTICSEARCH_MAX_QUEUE_LENGTH + + if CFG_ELASTICSEARCH_FLUSH_INTERVAL == -1: + config['interval'] = None + else: + config['interval'] = CFG_ELASTICSEARCH_FLUSH_INTERVAL + + lj = lumberjack.Lumberjack( + hosts=CFG_ELASTICSEARCH_HOSTS, + config=config) + + handler = lj.get_handler(suffix_format=CFG_ELASTICSEARCH_SUFFIX_FORMAT) + logging.getLogger('events').addHandler(handler) + logging.getLogger('events').setLevel(logging.INFO) + + logging.getLogger('lumberjack').addHandler( + logging.StreamHandler(sys.stderr)) + logging.getLogger('lumberjack').setLevel(logging.ERROR) + + return lj + +LUMBERJACK = initialise_lumberjack() + +def register_schema(*args, **kwargs): + if not CFG_ELASTICSEARCH_LOGGING: + return None + return LUMBERJACK.register_schema(*args, **kwargs) diff --git a/modules/miscutil/lib/inveniocfg.py b/modules/miscutil/lib/inveniocfg.py index 45132b5f0d..8f298dd83a 100644 --- a/modules/miscutil/lib/inveniocfg.py +++ b/modules/miscutil/lib/inveniocfg.py @@ -190,6 +190,7 @@ def convert_conf_option(option_name, option_value): 'CFG_BIBSCHED_INCOMPATIBLE_TASKS', 'CFG_ICON_CREATION_FORMAT_MAPPINGS', 'CFG_BIBEDIT_AUTOCOMPLETE']: + 'CFG_ELASTICSEARCH_HOSTS']: try: option_value = option_value[1:-1] if option_name == "CFG_BIBEDIT_EXTEND_RECORD_WITH_COLLECTION_TEMPLATE" and option_value.strip().startswith("{"): diff --git a/requirements.txt b/requirements.txt index 6f17065ccc..72e3a33885 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ redis==2.9.0 nydus==0.10.6 Cerberus==0.5 matplotlib +git+https://github.com/CERNDocumentServer/lumberjack.git From c8fb254185b4cb53ac2ad7acce2154b99152375d Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis Date: Thu, 4 Dec 2014 15:21:09 +0100 Subject: [PATCH 18/83] WebStat: register custom events on es * Adds `loanrequest` schema for `Lumberjack`. * Adds custom events elastic search logging. Signed-off-by: Harris Tzovanakis --- modules/webstat/lib/webstat.py | 48 ++++++++++++++++++++++----- modules/webstat/lib/webstat_config.py | 26 ++++++++++++++- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/modules/webstat/lib/webstat.py b/modules/webstat/lib/webstat.py index f5e440eea6..7232fc052e 100644 --- a/modules/webstat/lib/webstat.py +++ b/modules/webstat/lib/webstat.py @@ -26,6 +26,7 @@ import calendar from datetime import timedelta from urllib import quote +from logging import getLogger from invenio import template from invenio.config import \ @@ -33,8 +34,11 @@ CFG_TMPDIR, \ CFG_SITE_URL, \ CFG_SITE_LANG, \ - CFG_WEBSTAT_BIBCIRCULATION_START_YEAR -from invenio.webstat_config import CFG_WEBSTAT_CONFIG_PATH + CFG_WEBSTAT_BIBCIRCULATION_START_YEAR, \ + CFG_ELASTICSEARCH_LOGGING +from invenio.webstat_config import \ + CFG_WEBSTAT_CONFIG_PATH, \ + CFG_ELASTICSEARCH_EVENTS_MAP from invenio.bibindex_engine_utils import get_all_indexes from invenio.bibindex_tokenizers.BibIndexJournalTokenizer import CFG_JOURNAL_TAG from invenio.search_engine import get_coll_i18nname, \ @@ -724,6 +728,7 @@ def destroy_customevents(): msg += destroy_customevent(event[0]) return msg + def register_customevent(event_id, *arguments): """ Registers a custom event. Will add to the database's event tables @@ -739,17 +744,25 @@ def register_customevent(event_id, *arguments): @param *arguments: The rest of the parameters of the function call @type *arguments: [params] """ - res = run_sql("SELECT CONCAT('staEVENT', number),cols " + \ - "FROM staEVENT WHERE id = %s", (event_id, )) + query = """ + SELECT CONCAT('staEVENT', number), + cols + FROM staEVENT + WHERE id = %s + """ + params = (event_id,) + res = run_sql(query, params) + # the id does not exist if not res: - return # the id does not exist + return tbl_name = res[0][0] if res[0][1]: col_titles = cPickle.loads(res[0][1]) else: col_titles = [] if len(col_titles) != len(arguments[0]): - return # there is different number of arguments than cols + # there is different number of arguments than cols + return # Make sql query if len(arguments[0]) != 0: @@ -758,18 +771,35 @@ def register_customevent(event_id, *arguments): for title in col_titles: sql_query.append("`%s`" % title) sql_query.append(",") - sql_query.pop() # del the last ',' + # del the last ',' + sql_query.pop() sql_query.append(") VALUES (") for argument in arguments[0]: sql_query.append("%s") sql_query.append(",") sql_param.append(argument) - sql_query.pop() # del the last ',' + # del the last ',' + sql_query.pop() sql_query.append(")") sql_str = ''.join(sql_query) run_sql(sql_str, tuple(sql_param)) + + # Register the event on elastic search + if CFG_ELASTICSEARCH_LOGGING and event_id in \ + CFG_ELASTICSEARCH_EVENTS_MAP.keys(): + # Initialize elastic search handler + elastic_search_parameters = zip(col_titles, arguments[0]) + event_logger_name = "events.{0}".format(event_id) + logger = getLogger(event_logger_name) + log_event = {} + for key, value in elastic_search_parameters: + log_event[key] = value + logger.info(log_event) else: - run_sql("INSERT INTO %s () VALUES ()" % wash_table_column_name(tbl_name)) # kwalitee: disable=sql + # kwalitee: disable=sql + run_sql( + "INSERT INTO %s () VALUES ()" % wash_table_column_name(tbl_name) + ) def cache_keyevent_trend(ids=[]): diff --git a/modules/webstat/lib/webstat_config.py b/modules/webstat/lib/webstat_config.py index 917dd71aac..da93dc732d 100644 --- a/modules/webstat/lib/webstat_config.py +++ b/modules/webstat/lib/webstat_config.py @@ -21,6 +21,30 @@ __revision__ = "$Id$" -from invenio.config import CFG_ETCDIR +from invenio.config import CFG_ETCDIR, CFG_ELASTICSEARCH_LOGGING, CFG_CERN_SITE CFG_WEBSTAT_CONFIG_PATH = CFG_ETCDIR + "/webstat/webstat.cfg" + +if CFG_CERN_SITE: + CFG_ELASTICSEARCH_EVENTS_MAP = { + "events.loanrequest": { + "_source": { + "enabled": True + }, + "properties": { + "request_id": { + "type": "integer" + }, + "load_id": { + "type": "integer" + } + } + } + } +else: + CFG_ELASTICSEARCH_EVENTS_MAP = {} + +if CFG_ELASTICSEARCH_LOGGING: + from invenio.elasticsearch_logging import register_schema + for name, arguments in CFG_ELASTICSEARCH_EVENTS_MAP.items(): + register_schema(name, arguments) From ab6d138c6777fbbc4849a4b08c000e9d659d0e87 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Fri, 12 Dec 2014 12:00:05 +0100 Subject: [PATCH 19/83] BibFormat: Read record_stats from ES (if enabled) * Record stats are being loaded from elasticsearch if elasticsearch is enabled (if not, they are being loaded from DB as before). Signed-off-by: Sebastian Witowski --- .../lib/elements/bfe_record_stats.py | 250 ++++++++++++++++-- 1 file changed, 224 insertions(+), 26 deletions(-) diff --git a/modules/bibformat/lib/elements/bfe_record_stats.py b/modules/bibformat/lib/elements/bfe_record_stats.py index e9da239a6f..bcd98ca16c 100644 --- a/modules/bibformat/lib/elements/bfe_record_stats.py +++ b/modules/bibformat/lib/elements/bfe_record_stats.py @@ -19,6 +19,23 @@ __revision__ = "$Id$" from invenio.dbquery import run_sql +ELASTICSEARCH_ENABLED = False + +try: + from elasticsearch import Elasticsearch + from invenio.config import \ + CFG_ELASTICSEARCH_LOGGING, \ + CFG_ELASTICSEARCH_SEARCH_HOST, \ + CFG_ELASTICSEARCH_INDEX_PREFIX + + # if we were able to import all modules and ES logging is enabled, then use + # elasticsearch instead of normal db queries + if CFG_ELASTICSEARCH_LOGGING: + ELASTICSEARCH_ENABLED = True +except ImportError: + pass + # elasticsearch not supported + def format_element(bfo, display='day_distinct_ip_nb_views'): ''' @@ -26,31 +43,212 @@ def format_element(bfo, display='day_distinct_ip_nb_views'): @param display: the type of statistics displayed. Can be 'total_nb_view', 'day_nb_views', 'total_distinct_ip_nb_views', 'day_distincts_ip_nb_views', 'total_distinct_ip_per_day_nb_views' ''' + if ELASTICSEARCH_ENABLED: + page_views = 0 + ES_INDEX = CFG_ELASTICSEARCH_INDEX_PREFIX + "*" + recID = bfo.recID + query = "" + + es = Elasticsearch(CFG_ELASTICSEARCH_SEARCH_HOST) + if display == 'total_nb_views': + query = { + "query": { + "bool": { + "must": [ + { + "match": { + "id_bibrec": recID + } + }, + { + "match": { + "_type": "events.pageviews" + } + } + ] + } + } + } + results = es.count(index=ES_INDEX, body=query) + if results: + page_views = results.get('count', 0) + elif display == 'day_nb_views': + query = { + "query": { + "filtered": { + "query": { + "bool": { + "must": [ + { + "match": { + "id_bibrec": recID + } + }, + { + "match": { + "_type": "events.pageviews" + } + } + ] + } + }, + "filter": { + "range": { + "@timestamp": { + "gt": "now-1d" + } + } + } + } + } + } + results = es.count(index=ES_INDEX, body=query) + if results: + page_views = results.get('count', 0) + elif display == 'total_distinct_ip_nb_views': + search_type = "count" + # TODO this search query with aggregation is slow, maybe there is a way to make it faster ? + query = { + "query": { + "bool": { + "must": [ + { + "match": { + "id_bibrec": recID + } + }, + { + "match": { + "_type": "events.pageviews" + } + } + ] + } + }, + "aggregations": { + "distinct_ips": { + "cardinality": { + "field": "client_host" + } + } + } + } + results = es.search(index=ES_INDEX, body=query, search_type=search_type) + if results: + page_views = results.get('aggregations', {}).get('distinct_ips', {}).get('value', 0) + elif display == 'day_distinct_ip_nb_views': + search_type = "count" + # TODO aggregation is slow, maybe there is a way to make a faster query + query = { + "query": { + "filtered": { + "query": { + "bool": { + "must": [ + { + "match": { + "id_bibrec": recID + } + }, + { + "match": { + "_type": "events.pageviews" + } + } + ] + } + }, + "filter": { + "range": { + "@timestamp": { + "gt": "now-1d" + } + } + } + } + }, + "aggregations": { + "distinct_ips": { + "cardinality": { + "field": "client_host" + } + } + } + } + results = es.search(index=ES_INDEX, body=query, search_type=search_type) + if results: + page_views = results.get('aggregations', {}).get('distinct_ips', {}).get('value', 0) + elif display == 'total_distinct_ip_per_day_nb_views': + search_type = "count" + # TODO aggregation is slow, maybe there is a way to make a faster query + query = { + "query": { + "filtered": { + "query": { + "bool": { + "must": [ + { + "match": { + "id_bibrec": recID + } + }, + { + "match": { + "_type": "events.pageviews" + } + } + ] + } + } + } + }, + "aggregations": { + "daily_stats": { + "date_histogram": { + "field": "@timestamp", + "interval": "day" + }, + "aggregations": { + "distinct_ips": { + "cardinality": { + "field": "client_host" + } + } + } + } + } + } + results = es.search(index=ES_INDEX, body=query, search_type=search_type) + if results: + buckets = results.get("aggregations", {}).get("daily_stats", {}).get("buckets", {}) + page_views = sum([int(bucket.get("distinct_ips", {}).get('value', '0')) for bucket in buckets]) + return page_views + else: - if display == 'total_nb_views': - return run_sql("""SELECT COUNT(client_host) FROM rnkPAGEVIEWS - WHERE id_bibrec=%s""", - (bfo.recID,))[0][0] - elif display == 'day_nb_views': - return run_sql("""SELECT COUNT(client_host) FROM rnkPAGEVIEWS - WHERE id_bibrec=%s AND DATE(view_time)=CURDATE()""", - (bfo.recID,))[0][0] - elif display == 'total_distinct_ip_nb_views': - return run_sql("""SELECT COUNT(DISTINCT client_host) FROM rnkPAGEVIEWS - WHERE id_bibrec=%s""", - (bfo.recID,))[0][0] - elif display == 'day_distinct_ip_nb_views': - return run_sql("""SELECT COUNT(DISTINCT client_host) FROM rnkPAGEVIEWS - WHERE id_bibrec=%s AND DATE(view_time)=CURDATE()""", - (bfo.recID,))[0][0] - elif display == 'total_distinct_ip_per_day_nb_views': - # Count the number of distinct IP addresses for every day Then - # sum up. Similar to total_distinct_users_nb_views but assume - # that several different users can be behind a single IP - # (which could change every day) - res = run_sql("""SELECT COUNT(DISTINCT client_host) - FROM rnkPAGEVIEWS - WHERE id_bibrec=%s GROUP BY DATE(view_time)""", - (bfo.recID,)) - return sum([row[0] for row in res]) + if display == 'total_nb_views': + return run_sql("""SELECT COUNT(client_host) FROM rnkPAGEVIEWS + WHERE id_bibrec=%s""", + (bfo.recID,))[0][0] + elif display == 'day_nb_views': + return run_sql("""SELECT COUNT(client_host) FROM rnkPAGEVIEWS + WHERE id_bibrec=%s AND DATE(view_time)=CURDATE()""", + (bfo.recID,))[0][0] + elif display == 'total_distinct_ip_nb_views': + return run_sql("""SELECT COUNT(DISTINCT client_host) FROM rnkPAGEVIEWS + WHERE id_bibrec=%s""", + (bfo.recID,))[0][0] + elif display == 'day_distinct_ip_nb_views': + return run_sql("""SELECT COUNT(DISTINCT client_host) FROM rnkPAGEVIEWS + WHERE id_bibrec=%s AND DATE(view_time)=CURDATE()""", + (bfo.recID,))[0][0] + elif display == 'total_distinct_ip_per_day_nb_views': + # Count the number of distinct IP addresses for every day Then + # sum up. Similar to total_distinct_users_nb_views but assume + # that several different users can be behind a single IP + # (which could change every day) + res = run_sql("""SELECT COUNT(DISTINCT client_host) + FROM rnkPAGEVIEWS + WHERE id_bibrec=%s GROUP BY DATE(view_time)""", + (bfo.recID,)) + return sum([row[0] for row in res]) From b4ad718e1cfe6e8e2597bcc7e16b90ea935b2722 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Thu, 18 Dec 2014 16:11:02 +0100 Subject: [PATCH 20/83] WebSearch: none external collection list fix * FIX Handles the scenario where the `selected_external_collections_infos` is `None`. Signed-off-by: Esteban J. G. Gabancho --- modules/websearch/lib/websearch_external_collections.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/websearch/lib/websearch_external_collections.py b/modules/websearch/lib/websearch_external_collections.py index fedee738a7..b0b705acdb 100644 --- a/modules/websearch/lib/websearch_external_collections.py +++ b/modules/websearch/lib/websearch_external_collections.py @@ -189,6 +189,8 @@ def do_external_search(req, lang, vprint, basic_search_units, search_engines, pr vprint(3, 'beginning external search') engines_list = [] + search_engines = search_engines or list() + for engine in search_engines: url = engine.build_search_url(basic_search_units, req.args, lang) user_url = engine.build_user_search_url(basic_search_units, req.args, lang) From 1fc9ee559e6eb16c7c59b95e0e7f7287653ec3d4 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Mon, 12 Jan 2015 14:11:59 +0100 Subject: [PATCH 21/83] containerutils: new lazy data structures * Back ports lazy dictionaries from next. Signed-off-by: Esteban J. G. Gabancho --- modules/miscutil/lib/containerutils.py | 142 +++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/modules/miscutil/lib/containerutils.py b/modules/miscutil/lib/containerutils.py index f40cfa2d3e..f0d172e67f 100644 --- a/modules/miscutil/lib/containerutils.py +++ b/modules/miscutil/lib/containerutils.py @@ -20,6 +20,7 @@ """ import re +from six import iteritems def get_substructure(data, path): """ @@ -274,3 +275,144 @@ def set(self, key, value, extend=False): def update(self, E, **F): self._dict.update(E, **F) + + +class LazyDict(object): + + """Lazy dictionary that evaluates its content when it is first accessed. + + Example: + + .. code-block:: python + + def my_dict(): + from werkzeug.utils import import_string + return {'foo': import_string('foo')} + + lazy_dict = LazyDict(my_dict) + # at this point the internal dictionary is empty + lazy_dict['foo'] + """ + + def __init__(self, function=dict): + """Initialize lazy dictionary with given function. + + :param function: it must return a dictionary like structure + """ + super(LazyDict, self).__init__() + self._cached_dict = None + self._function = function + + def _evaluate_function(self): + self._cached_dict = self._function() + + def __getitem__(self, key): + """Return item from cache if it exists else create it.""" + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.__getitem__(key) + + def __setitem__(self, key, value): + """Set item to cache if it exists else create it.""" + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.__setitem__(key, value) + + def __delitem__(self, key): + """Delete item from cache if it exists else create it.""" + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.__delitem__(key) + + def __getattr__(self, key): + """Get cache attribute if it exists else create it.""" + if self._cached_dict is None: + self._evaluate_function() + return getattr(self._cached_dict, key) + + def __iter__(self): + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.__iter__() + + def iteritems(self): + if self._cached_dict is None: + self._evaluate_function() + return iteritems(self._cached_dict) + + def iterkeys(self): + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.iterkeys() + + def itervalues(self): + if self._cached_dict is None: + self._evaluate_function() + return self._cached_dict.itervalues() + + def expunge(self): + self._cached_dict = None + + def get(self, key, default=None): + try: + return self.__getitem__(key) + except KeyError: + return default + + +class LaziestDict(LazyDict): + + """Even lazier dictionary (maybe the laziest). + + It does not have content and when a key is accessed it tries to evaluate + only this key. + + Example: + + .. code-block:: python + + def reader_discover(key): + from werkzeug.utils import import_string + return import_string( + 'invenio.jsonalchemy.jsonext.readers%sreader:reader' % (key) + ) + + laziest_dict = LaziestDict(reader_discover) + + laziest_dict['json'] + # It will give you the JsonReader class + """ + + def __init__(self, function=dict): + """Initialize laziest dictionary with given function. + + :param function: it must accept one parameter (the key of the + dictionary) and returns the element which will be store that key. + """ + super(LaziestDict, self).__init__(function) + + def _evaluate_function(self): + """Create empty dict if necessary.""" + if self._cached_dict is None: + self._cached_dict = {} + + def __getitem__(self, key): + if self._cached_dict is None: + self._evaluate_function() + if key not in self._cached_dict: + try: + self._cached_dict.__setitem__(key, self._function(key)) + except: + raise KeyError(key) + return self._cached_dict.__getitem__(key) + + def __contains__(self, key): + if self._cached_dict is None: + self._evaluate_function() + if key not in self._cached_dict: + try: + self.__getitem__(key) + except: + return False + return True + From 0e400652fb8e8cb46feed99117b5107d3cb1e535 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Wed, 11 Feb 2015 15:56:45 +0100 Subject: [PATCH 22/83] xmlDict: initial release backport Signed-off-by: Esteban J. G. Gabancho --- modules/miscutil/lib/Makefile.am | 1 + modules/miscutil/lib/xmlDict.py | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 modules/miscutil/lib/xmlDict.py diff --git a/modules/miscutil/lib/Makefile.am b/modules/miscutil/lib/Makefile.am index 2d094d2711..fdd4cc7704 100644 --- a/modules/miscutil/lib/Makefile.am +++ b/modules/miscutil/lib/Makefile.am @@ -84,6 +84,7 @@ pylib_DATA = __init__.py \ xapianutils_bibrank_indexer.py \ xapianutils_bibrank_searcher.py \ xapianutils_config.py \ + xmlDict.py \ remote_debugger.py \ remote_debugger_config.py \ remote_debugger_wsgi_reload.py \ diff --git a/modules/miscutil/lib/xmlDict.py b/modules/miscutil/lib/xmlDict.py new file mode 100644 index 0000000000..2634b9d0a2 --- /dev/null +++ b/modules/miscutil/lib/xmlDict.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +## The following code is authored by Duncan McGreggor and is licensed +## under PSF license. It was taken from +## . + +import six +import xml.etree.ElementTree as ElementTree + + +class XmlListConfig(list): + def __init__(self, aList): + for element in aList: + if element: + # treat like dict + if len(element) == 1 or element[0].tag != element[1].tag: + self.append(XmlDictConfig(element)) + # treat like list + elif element[0].tag == element[1].tag: + self.append(XmlListConfig(element)) + elif element.text: + text = element.text.strip() + if text: + self.append(text) + + +class XmlDictConfig(dict): + ''' + Example usage: + + >>> tree = ElementTree.parse('your_file.xml') + >>> root = tree.getroot() + >>> xmldict = XmlDictConfig(root) + + Or, if you want to use an XML string: + + >>> root = ElementTree.XML(xml_string) + >>> xmldict = XmlDictConfig(root) + + And then use xmldict for what it is... a dict. + ''' + def __init__(self, parent_element): + if parent_element.items(): + self.update(dict(parent_element.items())) + + for element in parent_element: + if element: + # treat like dict - we assume that if the first two tags + # in a series are different, then they are all different. + if len(element) == 1 or element[0].tag != element[1].tag: + aDict = XmlDictConfig(element) + # treat like list - we assume that if the first two tags + # in a series are the same, then the rest are the same. + else: + # here, we put the list in dictionary; the key is the + # tag name the list elements all share in common, and + # the value is the list itself + aDict = {element[0].tag: XmlListConfig(element)} + # if the tag has attributes, add those to the dict + if element.items(): + aDict.update(dict(element.items())) + self.update({element.tag: aDict}) + # this assumes that if you've got an attribute in a tag, + # you won't be having any text. This may or may not be a + # good idea -- time will tell. It works for the way we are + # currently doing XML configuration files... + elif element.items(): + + # this assumes that if we got a single attribute + # with no children the attribute defines the type of the text + if len(element.items()) == 1 and not list(element): + # check if its str or unicode and if the text is empty, + # otherwise the tag has empty text, no need to add it + if isinstance(element.text, six.string_types) and element.text.strip() != '': + # we have an attribute in the tag that specifies + # most probably the type of the text + tag = element.items()[0][1] + self.update({element.tag: dict({tag: element.text})}) + else: + self.update({element.tag: dict(element.items())}) + if not list(element) and isinstance(element.text, six.string_types)\ + and element.text.strip() != '': + self[element.tag].update(dict({"text": element.text})) + # finally, if there are no child tags and no attributes, extract + # the text + else: + self.update({element.tag: element.text}) + From b70e8452689c09a814099edd42537c38bbe0ebad Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Wed, 11 Feb 2015 16:33:38 +0100 Subject: [PATCH 23/83] pidstore: initial release backport Signed-off-by: Esteban J. G. Gabancho --- configure.ac | 1 + modules/miscutil/lib/Makefile.am | 4 +- modules/miscutil/lib/dataciteutils.py | 177 +++++-- modules/miscutil/lib/inveniocfg.py | 3 +- modules/miscutil/lib/pid_provider.py | 168 +++++++ .../miscutil/lib/pid_providers/Makefile.am | 25 + .../miscutil/lib/pid_providers/__init__.py | 18 + .../miscutil/lib/pid_providers/datacite.py | 203 ++++++++ .../miscutil/lib/pid_providers/local_doi.py | 45 ++ modules/miscutil/lib/pid_store.py | 432 ++++++++++++++++++ .../invenio_2015_01_15_pidstore_initial.py | 90 ++++ modules/miscutil/sql/tabbibclean.sql | 2 + modules/miscutil/sql/tabcreate.sql | 30 ++ 13 files changed, 1167 insertions(+), 31 deletions(-) create mode 100644 modules/miscutil/lib/pid_provider.py create mode 100644 modules/miscutil/lib/pid_providers/Makefile.am create mode 100644 modules/miscutil/lib/pid_providers/__init__.py create mode 100644 modules/miscutil/lib/pid_providers/datacite.py create mode 100644 modules/miscutil/lib/pid_providers/local_doi.py create mode 100644 modules/miscutil/lib/pid_store.py create mode 100644 modules/miscutil/lib/upgrades/invenio_2015_01_15_pidstore_initial.py diff --git a/configure.ac b/configure.ac index 9cce278beb..ef1f6df786 100644 --- a/configure.ac +++ b/configure.ac @@ -811,6 +811,7 @@ AC_CONFIG_FILES([config.nice \ modules/miscutil/etc/ckeditor_scientificchar/lang/Makefile \ modules/miscutil/lib/Makefile \ modules/miscutil/lib/upgrades/Makefile \ + modules/miscutil/lib/pid_providers/Makefile \ modules/miscutil/sql/Makefile \ modules/miscutil/web/Makefile \ modules/webaccess/Makefile \ diff --git a/modules/miscutil/lib/Makefile.am b/modules/miscutil/lib/Makefile.am index fdd4cc7704..734f528f2d 100644 --- a/modules/miscutil/lib/Makefile.am +++ b/modules/miscutil/lib/Makefile.am @@ -15,7 +15,7 @@ # along with Invenio; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -SUBDIRS = upgrades +SUBDIRS = upgrades pid_providers pylibdir = $(libdir)/python/invenio @@ -65,6 +65,8 @@ pylib_DATA = __init__.py \ pluginutils.py \ pluginutils_unit_tests.py \ redisutils.py \ + pid_provider.py \ + pid_store.py \ plotextractor.py \ plotextractor_converter.py \ plotextractor_getter.py \ diff --git a/modules/miscutil/lib/dataciteutils.py b/modules/miscutil/lib/dataciteutils.py index ef74a5b51d..ea8811487a 100644 --- a/modules/miscutil/lib/dataciteutils.py +++ b/modules/miscutil/lib/dataciteutils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2012 CERN. +# Copyright (C) 2012, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -31,7 +31,11 @@ Example of usage: doc = ''' - + 10.5072/invenio.test.1 @@ -78,8 +82,12 @@ if not HAS_SSL: from warnings import warn - warn("Module ssl not installed. Please install with e.g. 'pip install ssl'. Required for HTTPS connections to DataCite.", RuntimeWarning) + warn("Module ssl not installed. Please install with e.g. " + "'pip install ssl'. Required for HTTPS connections to DataCite.", + RuntimeWarning) +import re +from invenio.xmlDict import XmlDictConfig, ElementTree # Uncomment to enable debugging of HTTP connection and uncomment line in # DataCiteRequest.request() @@ -93,15 +101,18 @@ # OpenSSL 1.0.0 has a reported bug with SSLv3/TLS handshake. # Python libs affected are httplib2 and urllib2. Eg: # httplib2.SSLHandshakeError: [Errno 1] _ssl.c:497: - # error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error - # custom HTTPS opener, banner's oracle 10g server supports SSLv3 only + # error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal + # error custom HTTPS opener, banner's oracle 10g server supports SSLv3 only class HTTPSConnectionV3(httplib.HTTPSConnection): def __init__(self, *args, **kwargs): httplib.HTTPSConnection.__init__(self, *args, **kwargs) def connect(self): try: - sock = socket.create_connection((self.host, self.port), self.timeout) + sock = socket.create_connection( + (self.host, self.port), + self.timeout + ) except AttributeError: # Python 2.4 compatibility (does not deal with IPv6) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -116,9 +127,15 @@ def connect(self): pass try: - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv3) + self.sock = ssl.wrap_socket( + sock, self.key_file, self.cert_file, + ssl_version=ssl.PROTOCOL_TLSv1 + ) except ssl.SSLError: - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_SSLv23) + self.sock = ssl.wrap_socket( + sock, self.key_file, self.cert_file, + ssl_version=ssl.PROTOCOL_SSLv23 + ) class HTTPSHandlerV3(urllib2.HTTPSHandler): def https_open(self, req): @@ -176,7 +193,10 @@ class DataCiteRequestError(DataCiteError): class DataCiteNoContentError(DataCiteRequestError): - """ DOI is known to MDS, but is not resolvable (might be due to handle's latency) """ + """ + DOI is known to MDS, but is not resolvable (might be due to handle's + latency) + """ pass @@ -235,7 +255,8 @@ class DataCiteRequest(object): query string on all requests. @type default_params: dict """ - def __init__(self, base_url=None, username=None, password=None, default_params={}): + def __init__(self, base_url=None, username=None, password=None, + default_params={}): self.base_url = base_url self.username = username self.password = password @@ -265,7 +286,8 @@ def request(self, url, method='GET', body=None, params={}, headers={}): self.data = None self.code = None - headers['Authorization'] = 'Basic ' + base64.encodestring(self.username + ':' + self.password) + headers['Authorization'] = 'Basic ' + \ + base64.encodestring(self.username + ':' + self.password) if headers['Authorization'][-1] == '\n': headers['Authorization'] = headers['Authorization'][:-1] @@ -284,7 +306,8 @@ def request(self, url, method='GET', body=None, params={}, headers={}): # HTTP client requests must end with double newline (not added # by urllib2) body += '\r\n\r\n' - body = body.encode('utf-8') + if isinstance(body, unicode): + body = body.encode('utf-8') else: if params: url = "%s?%s" % (url, urlencode(params)) @@ -301,10 +324,10 @@ def request(self, url, method='GET', body=None, params={}, headers={}): res = opener.open(request) self.code = res.code self.data = res.read() - except urllib2.HTTPError, e: + except urllib2.HTTPError as e: self.code = e.code self.data = e.msg - except urllib2.URLError, e: + except urllib2.URLError as e: raise HttpError(e) def get(self, url, params={}, headers={}): @@ -313,11 +336,13 @@ def get(self, url, params={}, headers={}): def post(self, url, body=None, params={}, headers={}): """ Make a POST request """ - return self.request(url, method='POST', body=body, params=params, headers=headers) + return self.request(url, method='POST', body=body, params=params, + headers=headers) def delete(self, url, params={}, headers={}): """ Make a DELETE request """ - return self.request(url, method="DELETE", params=params, headers=headers) + return self.request(url, method="DELETE", params=params, + headers=headers) class DataCite(object): @@ -325,10 +350,11 @@ class DataCite(object): DataCite API wrapper """ - def __init__(self, username=None, password=None, url=None, prefix=None, test_mode=None, api_ver="2"): + def __init__(self, username=None, password=None, url=None, prefix=None, + test_mode=None, api_ver="2"): """ - Initialize DataCite API. In case parameters are not specified via keyword - arguments, they will be read from the Invenio configuration. + Initialize DataCite API. In case parameters are not specified via + keyword arguments, they will be read from the Invenio configuration. @param username: DataCite username (or CFG_DATACITE_USERNAME) @type username: str @@ -336,27 +362,37 @@ def __init__(self, username=None, password=None, url=None, prefix=None, test_mod @param password: DataCite password (or CFG_DATACITE_PASSWORD) @type password: str - @param url: DataCite API base URL (or CFG_DATACITE_URL). Defaults to https://mds.datacite.org/. + @param url: DataCite API base URL (or CFG_DATACITE_URL). Defaults to + https://mds.datacite.org/. @type url: str - @param prefix: DOI prefix (or CFG_DATACITE_DOI_PREFIX). Defaults to 10.5072 (DataCite test prefix). + @param prefix: DOI prefix (or CFG_DATACITE_DOI_PREFIX). Defaults to + 10.5072 (DataCite test prefix). @type prefix: str - @param test_mode: Set to True to enable test mode (or CFG_DATACITE_TESTMODE). Defaults to False. + @param test_mode: Set to True to enable test mode (or + CFG_DATACITE_TESTMODE). Defaults to False. @type test_mode: boolean - @param api_ver: DataCite API version. Currently has no effect. Default to 2. + @param api_ver: DataCite API version. Currently has no effect. + Default to 2. @type api_ver: str """ if not HAS_SSL: - warn("Module ssl not installed. Please install with e.g. 'pip install ssl'. Required for HTTPS connections to DataCite.") - - self.username = username or getattr(config, 'CFG_DATACITE_USERNAME', '') - self.password = password or getattr(config, 'CFG_DATACITE_PASSWORD', '') - self.prefix = prefix or getattr(config, 'CFG_DATACITE_DOI_PREFIX', '10.5072') + warn("Module ssl not installed. Please install with e.g. " + "'pip install ssl'. Required for HTTPS connections to " + "DataCite.") + + self.username = username or getattr(config, 'CFG_DATACITE_USERNAME', + '') + self.password = password or getattr(config, 'CFG_DATACITE_PASSWORD', + '') + self.prefix = prefix or getattr(config, 'CFG_DATACITE_DOI_PREFIX', + '10.5072') self.api_ver = api_ver # Currently not used - self.api_url = url or getattr(config, 'CFG_DATACITE_URL', 'https://mds.datacite.org/') + self.api_url = url or getattr(config, 'CFG_DATACITE_URL', + 'https://mds.datacite.org/') if self.api_url[-1] != '/': self.api_url = self.api_url + "/" @@ -535,3 +571,86 @@ def media_post(self, doi, media): return r.data else: raise DataCiteError.factory(r.code) + + +class DataciteMetadata(object): + + def __init__(self, doi): + + self.url = "http://data.datacite.org/application/x-datacite+xml/" + self.error = False + try: + data = urllib2.urlopen(self.url + doi).read() + except urllib2.HTTPError: + self.error = True + + if not self.error: + # Clean the xml for parsing + data = re.sub('<\?xml.*\?>', '', data, count=1) + + # Remove the resource tags + data = re.sub('', '', data) + self.data = '' + \ + data[0:len(data) - 11] + '' + self.root = ElementTree.XML(self.data) + self.xml = XmlDictConfig(self.root) + + def get_creators(self, attribute='creatorName'): + if 'creators' in self.xml: + if isinstance(self.xml['creators']['creator'], list): + return [c[attribute] for c in self.xml['creators']['creator']] + else: + return self.xml['creators']['creator'][attribute] + + return None + + def get_titles(self): + if 'titles' in self.xml: + return self.xml['titles']['title'] + return None + + def get_publisher(self): + if 'publisher' in self.xml: + return self.xml['publisher'] + return None + + def get_dates(self): + if 'dates' in self.xml: + if isinstance(self.xml['dates']['date'], dict): + return self.xml['dates']['date'].values()[0] + return self.xml['dates']['date'] + return None + + def get_publication_year(self): + if 'publicationYear' in self.xml: + return self.xml['publicationYear'] + return None + + def get_language(self): + if 'language' in self.xml: + return self.xml['language'] + return None + + def get_related_identifiers(self): + pass + + def get_description(self, description_type='Abstract'): + if 'descriptions' in self.xml: + if isinstance(self.xml['descriptions']['description'], list): + for description in self.xml['descriptions']['description']: + if description_type in description: + return description[description_type] + elif isinstance(self.xml['descriptions']['description'], dict): + description = self.xml['descriptions']['description'] + if description_type in description: + return description[description_type] + elif len(description) == 1: + # return the only description + return description.values()[0] + + return None + + def get_rights(self): + if 'titles' in self.xml: + return self.xml['rights'] + return None diff --git a/modules/miscutil/lib/inveniocfg.py b/modules/miscutil/lib/inveniocfg.py index 45132b5f0d..c716ec2d9d 100644 --- a/modules/miscutil/lib/inveniocfg.py +++ b/modules/miscutil/lib/inveniocfg.py @@ -245,7 +245,8 @@ def convert_conf_option(option_name, option_value): 'CFG_OAUTH2_PROVIDERS', 'CFG_BIBFORMAT_CACHED_FORMATS', 'CFG_BIBEDIT_ADD_TICKET_RT_QUEUES', - 'CFG_BIBAUTHORID_ENABLED_REMOTE_LOGIN_SYSTEMS',]: + 'CFG_BIBAUTHORID_ENABLED_REMOTE_LOGIN_SYSTEMS', + 'PIDSTORE_OBJECT_TYPES', ]: out = "[" for elem in option_value[1:-1].split(","): if elem: diff --git a/modules/miscutil/lib/pid_provider.py b/modules/miscutil/lib/pid_provider.py new file mode 100644 index 0000000000..fa6c70fa5c --- /dev/null +++ b/modules/miscutil/lib/pid_provider.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2015 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.containerutils import LazyDict + + +class PidProvider(object): + """ + Abstract class for persistent identifier provider classes. + + Subclasses must implement register, update, delete and is_provider_for_pid + methods and register itself: + + class MyProvider(PidProvider): + pid_type = "mypid" + + def reserve(self, pid, *args, **kwargs): + return True + + def register(self, pid, *args, **kwargs): + return True + + def update(self, pid, *args, **kwargs): + return True + + def delete(self, pid, *args, **kwargs): + try: + ... + except Exception as e: + pid.log("DELETE","Deletion failed") + return False + else: + pid.log("DELETE","Successfully deleted") + return True + + def is_provider_for_pid(self, pid_str): + pass + + PidProvider.register_provider(MyProvider) + + + The provider is responsible for handling of errors, as well as logging of + actions happening to the pid. See example above as well as the + DataCitePidProvider. + + Each method takes variable number of argument and keywords arguments. This + can be used to pass additional information to the provider when registering + a persistent identifier. E.g. a DOI requires URL and metadata to be able + to register the DOI. + """ + + def __load_providers(): + from invenio.pid_store import _PID_PROVIDERS + registry = dict() + for provider in _PID_PROVIDERS.values(): + if not issubclass(provider, PidProvider): + raise TypeError("Argument not an instance of PidProvider.") + pid_type = getattr(provider, 'pid_type', None) + if pid_type is None: + raise AttributeError( + "Provider must specify class variable pid_type.") + pid_type = pid_type.lower() + if pid_type not in registry: + registry[pid_type] = [] + + # Prevent double registration + if provider not in registry[pid_type]: + registry[pid_type].append(provider) + return registry + + registry = LazyDict(__load_providers) + """ Registry of possible providers """ + + pid_type = None + """ + Must be overwritten in subcleass and specified as a string (max len 6) + """ + + @staticmethod + def create(pid_type, pid_str, pid_provider, *args, **kwargs): + """ + Create a new instance of a PidProvider for the + given type and pid. + """ + providers = PidProvider.registry.get(pid_type.lower(), None) + for p in providers: + if p.is_provider_for_pid(pid_str): + return p(*args, **kwargs) + return None + + # + # API methods which must be implemented by each provider. + # + def reserve(self, pid, *args, **kwargs): + """ + Reserve a new persistent identifier + + This might or might not be useful depending on the service of the + provider. + """ + raise NotImplementedError + + def register(self, pid, *args, **kwargs): + """ Register a new persistent identifier """ + raise NotImplementedError + + def update(self, pid, *args, **kwargs): + """ Update information about a persistent identifier """ + raise NotImplementedError + + def delete(self, pid, *args, **kwargs): + """ Delete a persistent identifier """ + raise NotImplementedError + + def sync_status(self, pid, *args, **kwargs): + """ + Synchronize persistent identifier status with remote service provider. + """ + return True + + @classmethod + def is_provider_for_pid(cls, pid_str): + raise NotImplementedError + + # + # API methods which might need to be implemented depending on each provider. + # + def create_new_pid(self, pid_value): + """ Some PidProvider might have the ability to create new values """ + return pid_value + +class LocalPidProvider(PidProvider): + """ + Abstract class for local persistent identifier provides (i.e locally + unmanaged DOIs). + """ + def reserve(self, pid, *args, **kwargs): + pid.log("RESERVE", "Successfully reserved locally") + return True + + def register(self, pid, *args, **kwargs): + pid.log("REGISTER", "Successfully registered in locally") + return True + + def update(self, pid, *args, **kwargs): + # No logging necessary as status of PID is not changing + return True + + def delete(self, pid, *args, **kwargs): + """ Delete a registered DOI """ + pid.log("DELETE", "Successfully deleted locally") + return True diff --git a/modules/miscutil/lib/pid_providers/Makefile.am b/modules/miscutil/lib/pid_providers/Makefile.am new file mode 100644 index 0000000000..ded8a59605 --- /dev/null +++ b/modules/miscutil/lib/pid_providers/Makefile.am @@ -0,0 +1,25 @@ +## This file is part of Invenio. +## Copyright (C) 2015 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/pid_providers + +pylib_DATA = *.py + +EXTRA_DIST = $(pylib_DATA) + +CLEANFILES = *~ *.tmp *.pyc + diff --git a/modules/miscutil/lib/pid_providers/__init__.py b/modules/miscutil/lib/pid_providers/__init__.py new file mode 100644 index 0000000000..0eab0f8880 --- /dev/null +++ b/modules/miscutil/lib/pid_providers/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014 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. diff --git a/modules/miscutil/lib/pid_providers/datacite.py b/modules/miscutil/lib/pid_providers/datacite.py new file mode 100644 index 0000000000..4488c3671b --- /dev/null +++ b/modules/miscutil/lib/pid_providers/datacite.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014, 2015 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. + +""" + DataCite PID provider. +""" + +from invenio import config +from invenio.dataciteutils import DataCite as DataCiteUtil, HttpError, \ + DataCiteError, DataCiteGoneError, DataCiteNoContentError, \ + DataCiteNotFoundError + +from invenio.pid_provider import PidProvider +from invenio.pid_store import PIDSTORE_STATUS_NEW, \ + PIDSTORE_STATUS_REGISTERED, \ + PIDSTORE_STATUS_DELETED, \ + PIDSTORE_STATUS_RESERVED + + +class DataCite(PidProvider): + """ + DOI provider using DataCite API. + """ + pid_type = 'doi' + + def __init__(self): + self.api = DataCiteUtil() + + def _get_url(self, kwargs): + try: + return kwargs['url'] + except KeyError: + raise Exception("url keyword argument must be specified.") + + def _get_doc(self, kwargs): + try: + return kwargs['doc'] + except KeyError: + raise Exception("doc keyword argument must be specified.") + + def reserve(self, pid, *args, **kwargs): + """ Reserve a DOI (amounts to upload metadata, but not to mint) """ + # Only registered PIDs can be updated. + doc = self._get_doc(kwargs) + + try: + self.api.metadata_post(doc) + except DataCiteError as e: + pid.log("RESERVE", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("RESERVE", "Failed with HttpError - %s" % unicode(e)) + return False + else: + pid.log("RESERVE", "Successfully reserved in DataCite") + return True + + def register(self, pid, *args, **kwargs): + """ Register a DOI via the DataCite API """ + url = self._get_url(kwargs) + doc = self._get_doc(kwargs) + + try: + # Set metadata for DOI + self.api.metadata_post(doc) + # Mint DOI + self.api.doi_post(pid.pid_value, url) + except DataCiteError as e: + pid.log("REGISTER", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("REGISTER", "Failed with HttpError - %s" % unicode(e)) + return False + else: + pid.log("REGISTER", "Successfully registered in DataCite") + return True + + def update(self, pid, *args, **kwargs): + """ + Update metadata associated with a DOI. + + This can be called before/after a DOI is registered + + """ + url = self._get_url(kwargs) + doc = self._get_doc(kwargs) + + if pid.is_deleted(): + pid.log("UPDATE", "Reactivate in DataCite") + + try: + # Set metadata + self.api.metadata_post(doc) + self.api.doi_post(pid.pid_value, url) + except DataCiteError as e: + pid.log("UPDATE", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("UPDATE", "Failed with HttpError - %s" % unicode(e)) + return False + else: + if pid.is_deleted(): + pid.log( + "UPDATE", + "Successfully updated and possibly registered in DataCite" + ) + else: + pid.log("UPDATE", "Successfully updated in DataCite") + return True + + def delete(self, pid, *args, **kwargs): + """ Delete a registered DOI """ + try: + self.api.metadata_delete(pid.pid_value) + except DataCiteError as e: + pid.log("DELETE", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("DELETE", "Failed with HttpError - %s" % unicode(e)) + return False + else: + pid.log("DELETE", "Successfully deleted in DataCite") + return True + + def sync_status(self, pid, *args, **kwargs): + """ Synchronize DOI status DataCite MDS """ + status = None + + try: + self.api.doi_get(pid.pid_value) + status = PIDSTORE_STATUS_REGISTERED + except DataCiteGoneError: + status = PIDSTORE_STATUS_DELETED + except DataCiteNoContentError: + status = PIDSTORE_STATUS_REGISTERED + except DataCiteNotFoundError: + pass + except DataCiteError as e: + pid.log("SYNC", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("SYNC", "Failed with HttpError - %s" % unicode(e)) + return False + + if status is None: + try: + self.api.metadata_get(pid.pid_value) + status = PIDSTORE_STATUS_RESERVED + except DataCiteGoneError: + status = PIDSTORE_STATUS_DELETED + except DataCiteNoContentError: + status = PIDSTORE_STATUS_REGISTERED + except DataCiteNotFoundError: + pass + except DataCiteError as e: + pid.log("SYNC", "Failed with %s" % e.__class__.__name__) + return False + except HttpError as e: + pid.log("SYNC", "Failed with HttpError - %s" % unicode(e)) + return False + + if status is None: + status = PIDSTORE_STATUS_NEW + + if pid.status != status: + pid.log( + "SYNC", "Fixed status from %s to %s." % (pid.status, status) + ) + pid.status = status + + return True + + @classmethod + def is_provider_for_pid(cls, pid_str): + """ + Check if DataCite is the provider for this DOI + + Note: If you e.g. changed DataCite account and received a new prefix, + then this provider can only update and register DOIs for the new + prefix. + """ + CFG_DATACITE_DOI_PREFIX = getattr(config, + 'CFG_DATACITE_DOI_PREFIX', + '10.0572') + return pid_str.startswith("%s/" % CFG_DATACITE_DOI_PREFIX) + +provider = DataCite diff --git a/modules/miscutil/lib/pid_providers/local_doi.py b/modules/miscutil/lib/pid_providers/local_doi.py new file mode 100644 index 0000000000..adc57490a8 --- /dev/null +++ b/modules/miscutil/lib/pid_providers/local_doi.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2015 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. + +""" +LocalDOI provider. +""" + +from invenio import config + +from invenio.pid_provider import LocalPidProvider + + +class LocalDOI(LocalPidProvider): + """ + Provider for locally unmanaged DOIs. + """ + pid_type = 'doi' + + @classmethod + def is_provider_for_pid(cls, pid_str): + """ + Check if DOI is not the local datacite managed one. + """ + CFG_DATACITE_DOI_PREFIX = getattr(config, + 'CFG_DATACITE_DOI_PREFIX', + '10.5072') + return pid_str.startswith("%s/" % CFG_DATACITE_DOI_PREFIX) + +provider = LocalDOI diff --git a/modules/miscutil/lib/pid_store.py b/modules/miscutil/lib/pid_store.py new file mode 100644 index 0000000000..13f724ad9f --- /dev/null +++ b/modules/miscutil/lib/pid_store.py @@ -0,0 +1,432 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2015 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. + +"""PersistentIdentifier store and registration. + +Usage example for registering new identifiers:: + + from flask import url_for + from invenio.pid_store import PersistentIdentifier + + # Reserve a new DOI internally first + pid = PersistentIdentifier.create('doi','10.0572/1234') + + # Get an already reserved DOI + pid = PersistentIdentifier.get('doi', '10.0572/1234') + + # Assign it to a record. + pid.assign('rec', 1234) + + url = url_for("record.metadata", recid=1234, _external=True) + doc = " Date: Wed, 11 Feb 2015 16:34:27 +0100 Subject: [PATCH 24/83] BibFormat: DataCite3 export addition Signed-off-by: Esteban J. G. Gabancho --- .../etc/format_templates/DataCite3.xsl | 232 ++++++++++++++++++ .../etc/format_templates/Makefile.am | 1 + .../bibformat/etc/output_formats/DCITE3.bfo | 1 + .../bibformat/etc/output_formats/Makefile.am | 1 + 4 files changed, 235 insertions(+) create mode 100644 modules/bibformat/etc/format_templates/DataCite3.xsl create mode 100644 modules/bibformat/etc/output_formats/DCITE3.bfo diff --git a/modules/bibformat/etc/format_templates/DataCite3.xsl b/modules/bibformat/etc/format_templates/DataCite3.xsl new file mode 100644 index 0000000000..d76f311a8a --- /dev/null +++ b/modules/bibformat/etc/format_templates/DataCite3.xsl @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <xsl:value-of select="subfield[@code='a']"/> + <xsl:if test="subfield[@code='b']"> + <xsl:text>: </xsl:text><xsl:value-of select="subfield[@code='b']"/> + </xsl:if> + + + + + <xsl:value-of select="subfield[@code='a']"/> + <xsl:if test="subfield[@code='b']"> + <xsl:text>: </xsl:text><xsl:value-of select="subfield[@code='b']"/> + </xsl:if> + + + + + <xsl:value-of select="datafield[@tag=111]/subfield[@code='a']"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + + "", + + ] + + + + + diff --git a/modules/bibformat/etc/format_templates/Makefile.am b/modules/bibformat/etc/format_templates/Makefile.am index 5ef4d5b554..3cd74fd1c1 100644 --- a/modules/bibformat/etc/format_templates/Makefile.am +++ b/modules/bibformat/etc/format_templates/Makefile.am @@ -49,6 +49,7 @@ etc_DATA = Default_HTML_captions.bft \ Default_HTML_meta.bft \ WebAuthorProfile_affiliations_helper.bft \ DataCite.xsl \ + DataCite3.xsl \ Default_Mobile_brief.bft \ Default_Mobile_detailed.bft \ Authority_HTML_brief.bft \ diff --git a/modules/bibformat/etc/output_formats/DCITE3.bfo b/modules/bibformat/etc/output_formats/DCITE3.bfo new file mode 100644 index 0000000000..fc1af2db9d --- /dev/null +++ b/modules/bibformat/etc/output_formats/DCITE3.bfo @@ -0,0 +1 @@ +default: DataCite3.xsl diff --git a/modules/bibformat/etc/output_formats/Makefile.am b/modules/bibformat/etc/output_formats/Makefile.am index 2878a3ed0c..b5ecdf9af6 100644 --- a/modules/bibformat/etc/output_formats/Makefile.am +++ b/modules/bibformat/etc/output_formats/Makefile.am @@ -33,6 +33,7 @@ etc_DATA = HB.bfo \ HDFILE.bfo \ XD.bfo \ DCITE.bfo \ + DCITE3.bfo \ WAPAFF.bfo \ WAPDAT.bfo \ XW.bfo \ From 050b2bba46099fc31db4e31a12873fbd448060e4 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Thu, 5 Feb 2015 09:23:16 +0100 Subject: [PATCH 25/83] BibIndex: TermCollector empty authority id fix * Fixes exception on `_get_phrases_for_tokenizing` when it tries to index an authority tag which is empty. Reviewed-by: Samuele Kaplun Signed-off-by: Esteban J. G. Gabancho --- modules/bibindex/lib/bibindex_termcollectors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/bibindex/lib/bibindex_termcollectors.py b/modules/bibindex/lib/bibindex_termcollectors.py index 464cc31cc5..2a28e71f52 100644 --- a/modules/bibindex/lib/bibindex_termcollectors.py +++ b/modules/bibindex/lib/bibindex_termcollectors.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2014 CERN. +# Copyright (C) 2014, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -158,6 +158,8 @@ def _get_phrases_for_tokenizing(self, tag, recIDs): for recID in recIDs: control_nos = get_fieldvalues(recID, authority_tag) for control_no in control_nos: + if not control_no: + continue new_strings = get_index_strings_by_control_no(control_no) for string_value in new_strings: phrases.add((recID, string_value)) From 7044af6af4fb35c48f7cef31dabeb0d650eddf18 Mon Sep 17 00:00:00 2001 From: Raja Sripada Date: Mon, 15 Dec 2014 15:53:09 +0100 Subject: [PATCH 26/83] BibCirculation: new daemon task * Adds new task to update the metadata of the records corresponding to all the items in the BibCirc database with the shelf locations. Signed-off-by: Raja Sripada Reviewed-by: Esteban J. G. Gabancho --- .../lib/bibcirculation_daemon.py | 76 ++++++++++++++++++- .../lib/bibcirculation_dblayer.py | 53 +++++++++---- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/modules/bibcirculation/lib/bibcirculation_daemon.py b/modules/bibcirculation/lib/bibcirculation_daemon.py index dcfa8df371..5876622aa5 100644 --- a/modules/bibcirculation/lib/bibcirculation_daemon.py +++ b/modules/bibcirculation/lib/bibcirculation_daemon.py @@ -23,16 +23,21 @@ __revision__ = "$Id$" +import os import sys import time +import tempfile +from invenio.config import CFG_TMPDIR from invenio.dbquery import run_sql from invenio.bibtask import task_init, \ task_sleep_now_if_required, \ + task_low_level_submission, \ task_update_progress, \ task_set_option, \ task_get_option, \ write_message from invenio.mailutils import send_email +from invenio.search_engine_utils import get_fieldvalues import invenio.bibcirculation_dblayer as db from invenio.bibcirculation_config import CFG_BIBCIRCULATION_TEMPLATES, \ CFG_BIBCIRCULATION_LOANS_EMAIL, \ @@ -40,6 +45,8 @@ CFG_BIBCIRCULATION_REQUEST_STATUS_WAITING, \ CFG_BIBCIRCULATION_LOAN_STATUS_EXPIRED +from invenio.config import CFG_BIBCIRCULATION_ITEM_STATUS_ON_SHELF, \ + CFG_BIBCIRCULATION_ITEM_STATUS_ON_LOAN from invenio.bibcirculation_utils import generate_email_body, \ book_title_from_MARC, \ update_user_info_from_ldap, \ @@ -59,6 +66,8 @@ def task_submit_elaborate_specific_parameter(key, value, opts, args): task_set_option('update-borrowers', True) elif key in ('-r', '--update-requests'): task_set_option('update-requests', True) + elif key in ('-p', '--add-physical-copies-shelf-number-to-marc'): + task_set_option('add-physical-copies-shelf-number-to-marc', True) else: return False return True @@ -252,17 +261,80 @@ def task_run_core(): task_update_progress("ILL recall: processed %d out of %d expired ills." % (done+1, total_expired_ills)) write_message("Processed %d out of %d expired ills." % (done+1, total_expired_ills)) + if task_get_option("add-physical-copies-shelf-number-to-marc"): + write_message("Started adding info. reg. physical copies and shelf number to records") + modified_rec_locs = db.get_modified_items_physical_locations() + #Tagging of records + if modified_rec_locs: + total_modified_rec_locs = len(modified_rec_locs) + MARC_RECS_STR = "\n" + recids_seen = [] + for done, (recid, status, location, collection) in enumerate(modified_rec_locs): + if not int(recid) or not location or status not in [ CFG_BIBCIRCULATION_ITEM_STATUS_ON_SHELF, \ + CFG_BIBCIRCULATION_ITEM_STATUS_ON_LOAN ] or collection=='periodical' or\ + recid in recids_seen or 'DELETED' in get_fieldvalues(recid, '980__c'): + #or location in get_fieldvalues(recid, '852__h'): + continue + #MARC_RECS_STR: Compose a string with the records containing the controlfield(recid) and + #the 2 datafields(shelf no, physical copies) for each item retrieved from the query + copies = db.get_item_copies_details(recid) + MARC_RECS_STR += '' + str(recid) + '' + type_copies = get_fieldvalues(recid, '340__a') + if 'paper' not in type_copies: + MARC_RECS_STR += ' \ + paper \ + ' + if 'ebook' in type_copies or 'e-book' in type_copies: + MARC_RECS_STR += ' \ + ebook \ + ' + lib_loc_tuples = [] + for (_barcode, _loan_period, library_name, _library_id, + location, _nb_requests, _status, _collection, + _description, _due_date) in copies: + if not library_name or not location: continue + if not (library_name, location) in lib_loc_tuples: + lib_loc_tuples.append((library_name, location)) + else: continue + MARC_RECS_STR += ' \ + ' + library_name + ' \ + ' + location.replace('&', ' and ') +' \ + ' + MARC_RECS_STR += '' + recids_seen.append(recid) + # Upload chunks of 100 records and sleep if needed + if (done+1)%100 == 0 or (done+1) == total_modified_rec_locs: + MARC_RECS_STR += "" + timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.localtime()) + marcxmlfile = 'MARCxml_booksearch' + '_' + timestamp + '_' + fd, marcxmlfile = tempfile.mkstemp(dir=CFG_TMPDIR, prefix=marcxmlfile, suffix='.xml') + os.write(fd, MARC_RECS_STR) + os.close(fd) + write_message("Composed MARCXML saved into %s" % marcxmlfile) + #Schedule the bibupload task. + task_id = task_low_level_submission("bibupload", "BibCirc", "-c", marcxmlfile, '-P', '-3') + write_message("BibUpload scheduled with task id %s" % task_id) + write_message("Processed %d out of %d modified record locations." % (done+1, total_modified_rec_locs)) + MARC_RECS_STR = "\n" + task_sleep_now_if_required(can_stop_too=True) + + else: + write_message("No new records modified. Not scheduling any bibupload task") + return 1 + def main(): task_init(authorization_action='runbibcircd', authorization_msg="BibCirculation Task Submission", help_specific_usage="""-o, --overdue-letters\tCheck overdue loans and send recall emails if necessary.\n -b, --update-borrowers\tUpdate borrowers information from ldap.\n --r, --update-requests\tUpdate pending requests of users\n\n""", +-r, --update-requests\tUpdate pending requests of users\n +-p, --add-physical-copies-shelf-number-to-marc\tAdd info. reg. physical copies and shelf number to records' marc\n\n""", description="""Example: %s -u admin \n\n""" % (sys.argv[0]), - specific_params=("obr", ["overdue-letters", "update-borrowers", "update-requests"]), + specific_params=("obrp", ["overdue-letters", "update-borrowers", "update-requests", + "add-physical-copies-shelf-number-to-marc"]), task_submit_elaborate_specific_parameter_fnc=task_submit_elaborate_specific_parameter, version=__revision__, task_run_fnc = task_run_core diff --git a/modules/bibcirculation/lib/bibcirculation_dblayer.py b/modules/bibcirculation/lib/bibcirculation_dblayer.py index 42ecbce514..4ddf5a97ce 100644 --- a/modules/bibcirculation/lib/bibcirculation_dblayer.py +++ b/modules/bibcirculation/lib/bibcirculation_dblayer.py @@ -580,7 +580,7 @@ def get_pdf_request_data(status): it.id_bibrec=lr.id_bibrec AND lib.id = it.id_crcLIBRARY AND lr.status=%s; - """, (status,)) + """, (status, )) return res @@ -1050,6 +1050,14 @@ def get_loan_period(barcode): else: return None +def get_modified_items_physical_locations(): + """Get the physical locations of modified items.""" + res = run_sql("""SELECT id_bibrec, status, location, collection + FROM crcITEM + WHERE modification_date >= SUBDATE(NOW(),1) + AND modification_date <= NOW()""") + return res if res else None + def update_item_info(barcode, library_id, collection, location, description, loan_period, status, expected_arrival_date): """ @@ -1558,22 +1566,14 @@ def get_borrower_details(borrower_id): borrower_id: identify the borrower. It is also the primary key of the table crcBORROWER. """ - res = run_sql("""SELECT id, ccid, name, email, phone, address, mailbox - FROM crcBORROWER WHERE id=%s""", (borrower_id, )) + res = run_sql("""SELECT id, ccid, name, email, phone, address, mailbox + FROM crcBORROWER + WHERE id=%s""", (borrower_id, )) if res: - return clean_data(res[0]) + return res[0] else: return None - -def clean_data(data): - final_res = list(data) - for i in range(0, len(final_res)): - if isinstance(final_res[i], str): - final_res[i] = final_res[i].replace(",", " ") - return final_res - - def update_borrower_info(borrower_id, name, email, phone, address, mailbox): """ Update borrower info. @@ -1607,7 +1607,7 @@ def get_borrower_data(borrower_id): (borrower_id, )) if res: - return clean_data(res[0]) + return res[0] else: return None @@ -1622,7 +1622,7 @@ def get_borrower_data_by_id(borrower_id): WHERE id=%s""", (borrower_id, )) if res: - return clean_data(res[0]) + return res[0] else: return None @@ -1702,7 +1702,7 @@ def get_borrower_address(email): WHERE email=%s""", (email, )) if len(res[0][0]) > 0: - return res[0][0].replace(",", " ") + return res[0][0] else: return 0 @@ -1922,6 +1922,27 @@ def get_borrower_proposals(borrower_id): (borrower_id, CFG_BIBCIRCULATION_REQUEST_STATUS_PROPOSED)) return res +def get_borrower_ills(borrower_id): + """Get the ills of a borrower. + + :param borrower_id: identify the borrower. All the ills associated to this + borrower will be retrieved. It is also the primary key of the + `crcBORROWER` table. + """ + + res = run_sql(""" + SELECT item_info, + DATE_FORMAT(request_date,'%%Y-%%m-%%d'), + status, + DATE_FORMAT(due_date,'%%Y-%%m-%%d') + FROM crcILLREQUEST + WHERE id_crcBORROWER=%s and request_type='book' and + (status=%s or status=%s)""", + (borrower_id, CFG_BIBCIRCULATION_ILL_STATUS_REQUESTED, + CFG_BIBCIRCULATION_ILL_STATUS_ON_LOAN)) + return res + + def bor_loans_historical_overview(borrower_id): """ Get loans historical overview of a given borrower_id. From 006a499896573e3391d415ad653bfb8042e50dd2 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Mon, 16 Feb 2015 17:16:32 +0100 Subject: [PATCH 27/83] BibRank: no rank method fix * FIX Handles exception when trying to get the information on a rank method which does not exists. Reviewed-by: Samuele Kaplun Signed-off-by: Esteban J. G. Gabancho --- modules/bibrank/lib/bibrank_citation_searcher.py | 6 +++--- modules/bibrank/lib/bibrank_tag_based_indexer.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/bibrank/lib/bibrank_citation_searcher.py b/modules/bibrank/lib/bibrank_citation_searcher.py index 31e81dec16..05235688fe 100644 --- a/modules/bibrank/lib/bibrank_citation_searcher.py +++ b/modules/bibrank/lib/bibrank_citation_searcher.py @@ -79,10 +79,10 @@ def cache_filler(): def timestamp_verifier(): citation_lastupdate = get_lastupdated('citation') - if citation_lastupdate: + try: return citation_lastupdate.strftime("%Y-%m-%d %H:%M:%S") - else: - return "0000-00-00 00:00:00" + except AttributeError: + return citation_lastupdate DataCacher.__init__(self, cache_filler, timestamp_verifier) diff --git a/modules/bibrank/lib/bibrank_tag_based_indexer.py b/modules/bibrank/lib/bibrank_tag_based_indexer.py index a4478ef87e..239795a458 100644 --- a/modules/bibrank/lib/bibrank_tag_based_indexer.py +++ b/modules/bibrank/lib/bibrank_tag_based_indexer.py @@ -194,7 +194,8 @@ def get_lastupdated(rank_method_code): if res: return res[0][0] else: - raise Exception("Is this the first run? Please do a complete update.") + # raise Exception("Is this the first run? Please do a complete update.") + return "1970-01-01 00:00:00" def intoDB(dic, date, rank_method_code): """Insert the rank method data into the database""" @@ -209,6 +210,8 @@ def intoDB(dic, date, rank_method_code): def fromDB(rank_method_code): """Get the data for a rank method""" id = run_sql("SELECT id from rnkMETHOD where name=%s", (rank_method_code, )) + if not id: + return {} res = run_sql("SELECT relevance_data FROM rnkMETHODDATA WHERE id_rnkMETHOD=%s", (id[0][0], )) if res: return deserialize_via_marshal(res[0][0]) @@ -394,10 +397,7 @@ def add_recIDs_by_date(rank_method_code, dates=""): the ranking method RANK_METHOD_CODE. """ if not dates: - try: - dates = (get_lastupdated(rank_method_code), '') - except Exception: - dates = ("0000-00-00 00:00:00", '') + dates = (get_lastupdated(rank_method_code), '') if dates[0] is None: dates = ("0000-00-00 00:00:00", '') query = """SELECT b.id FROM bibrec AS b WHERE b.modification_date >= %s""" From f835be6f78df47eb177ffeef8915744041d859f0 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Tue, 17 Feb 2015 11:43:37 +0100 Subject: [PATCH 28/83] BibDocFile: disallow percent in filenames * Disallows % character in filenames since it may cause some problems with URL encoding/decoding leading to incorrect URLs. (addresses #1918) Signed-off-by: Sebastian Witowski --- .../bibdocfile/lib/bibdocfile_managedocfiles.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/bibdocfile/lib/bibdocfile_managedocfiles.py b/modules/bibdocfile/lib/bibdocfile_managedocfiles.py index 061baa5b41..1ef24b415d 100644 --- a/modules/bibdocfile/lib/bibdocfile_managedocfiles.py +++ b/modules/bibdocfile/lib/bibdocfile_managedocfiles.py @@ -599,7 +599,12 @@ def create_file_upload_interface(recid, os.unlink(uploaded_filepath) body += '' % \ _("You have already reached the maximum number of files for this type of document").replace('"', '\\"') - + elif '/' in filename or "%" in filename or "\\" in filename: + # We forbid usage of a few characters, for the good of + # everybody... + os.unlink(uploaded_filepath) + body += '' % \ + _("You are not allowed to use dot slash '/', percent '%' or backslash '\\\\' in file names. Please, rename the file and upload it again.").replace('"', '\\"') else: # Prepare to move file to # working_dir/files/updated/doctype/bibdocname/ @@ -645,13 +650,13 @@ def create_file_upload_interface(recid, body += '' % \ (_("A file with format '%s' already exists. Please upload another format.") % \ extension).replace('"', '\\"') - elif '.' in file_rename or '/' in file_rename or "\\" in file_rename or \ + elif '.' in file_rename or '/' in file_rename or "%" in file_rename or "\\" in file_rename or \ not os.path.abspath(new_uploaded_filepath).startswith(os.path.join(working_dir, 'files', 'updated')): # We forbid usage of a few characters, for the good of # everybody... os.unlink(uploaded_filepath) body += '' % \ - _("You are not allowed to use dot '.', slash '/', or backslash '\\\\' in file names. Choose a different name and upload your file again. In particular, note that you should not include the extension in the renaming field.").replace('"', '\\"') + _("You are not allowed to use dot '.', slash '/', percent '%' or backslash '\\\\' in file names. Choose a different name and upload your file again. In particular, note that you should not include the extension in the renaming field.").replace('"', '\\"') else: # No conflict with file name @@ -747,11 +752,11 @@ def create_file_upload_interface(recid, (_("A file named %s already exists. Please choose another name.") % \ file_rename).replace('"', '\\"') elif file_rename != file_target and \ - ('.' in file_rename or '/' in file_rename or "\\" in file_rename): + ('.' in file_rename or '/' in file_rename or '%' in file_rename or "\\" in file_rename): # We forbid usage of a few characters, for the good of # everybody... body += '' % \ - _("You are not allowed to use dot '.', slash '/', or backslash '\\\\' in file names. Choose a different name and upload your file again. In particular, note that you should not include the extension in the renaming field.").replace('"', '\\"') + _("You are not allowed to use dot '.', slash '/', percent '%' or backslash '\\\\' in file names. Choose a different name and upload your file again. In particular, note that you should not include the extension in the renaming field.").replace('"', '\\"') else: # Log log_action(working_dir, file_action, file_target, From 714899c9a369619889a2504a7b7d0c3c5b47e43a Mon Sep 17 00:00:00 2001 From: Ludmila Marian Date: Tue, 17 Feb 2015 11:21:36 +0100 Subject: [PATCH 29/83] BibConvert: retrieve arXiv license information * Adds the license information in the 540 tag when converting an OAI arXiv record to MARCXML. Signed-off-by: Ludmila Marian Co-authored-by: Jan Aage Lavik --- modules/bibconvert/etc/oaiarxiv2marcxml.xsl | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/bibconvert/etc/oaiarxiv2marcxml.xsl b/modules/bibconvert/etc/oaiarxiv2marcxml.xsl index c627ceb381..8a52f58fdc 100644 --- a/modules/bibconvert/etc/oaiarxiv2marcxml.xsl +++ b/modules/bibconvert/etc/oaiarxiv2marcxml.xsl @@ -2,7 +2,7 @@ + + + + arXiv + + + CC-BY-3.0 + + + CC-BY-NC-SA-3.0 + + + + + + From 3ea75669c694854b4faa83366ba2e7f7e4112317 Mon Sep 17 00:00:00 2001 From: Ludmila Marian Date: Wed, 18 Feb 2015 11:10:41 +0100 Subject: [PATCH 30/83] BibEdit: fix recID type * Various functions working with the recid expect it to be an integer, rather than a string. Signed-off-by: Ludmila Marian --- modules/bibformat/lib/elements/bfe_edit_record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bibformat/lib/elements/bfe_edit_record.py b/modules/bibformat/lib/elements/bfe_edit_record.py index 3fe43c4f42..cbeba2fcb3 100644 --- a/modules/bibformat/lib/elements/bfe_edit_record.py +++ b/modules/bibformat/lib/elements/bfe_edit_record.py @@ -36,7 +36,7 @@ def format_element(bfo, style): out = "" user_info = bfo.user_info - if user_can_edit_record_collection(user_info, bfo.recID): + if user_can_edit_record_collection(user_info, int(bfo.recID)): linkattrd = {} if style != '': linkattrd['style'] = style From dfc1a84e50a34c91ad88b42e87124bb572f264ce Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Wed, 18 Feb 2015 13:58:58 +0100 Subject: [PATCH 31/83] errorlib: sentry logging enhancement * Sets the level of the captured exception depending on the exception name. If the exception name does not match any on the default names, tries to get it from the `level` attribute of the exception, if any. Otherwise the default logging level is `ERROR`. Reviewed-by: Samuele Kaplun Signed-off-by: Esteban J. G. Gabancho --- modules/miscutil/lib/errorlib.py | 37 ++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/miscutil/lib/errorlib.py b/modules/miscutil/lib/errorlib.py index d2ceaaaf78..6b2114b8de 100644 --- a/modules/miscutil/lib/errorlib.py +++ b/modules/miscutil/lib/errorlib.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014 CERN. +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -29,6 +29,7 @@ import re import inspect import json +import logging from cStringIO import StringIO @@ -426,7 +427,9 @@ def default(self, obj): client.extra_context(user_info) filename = _get_filename_and_line(sys.exc_info())[0] client.tags_context({'filename': filename, 'version': CFG_VERSION}) - client.captureException() + client.captureException( + level=_guess_exception_level(sys.exc_info()) + ) except Exception: # Exception management of exception management try: @@ -607,15 +610,37 @@ def send_error_report_to_admin(header, url, time_msg, from invenio.mailutils import send_email send_email(from_addr, to_addr, subject="Error notification", content=body) + +def _guess_exception_level(exc_info): + """Set the logging level depending on the exception name.""" + try: + if hasattr(exc_info[1], 'level'): + return exc_info[1].level + + if 'warning' in exc_info[0].__name__.lower(): + return logging.WARN + if 'info' in exc_info[0].__name__.lower(): + return logging.INFO + if 'error' in exc_info[0].__name__.lower(): + return logging.ERROR + except AttributeError: + pass + + return logging.ERROR + + def _get_filename_and_line(exc_info): """ Return the filename, the line and the function_name where the exception happened. """ tb = exc_info[2] - exception_info = traceback.extract_tb(tb)[-1] - filename = os.path.basename(exception_info[0]) - line_no = exception_info[1] - function_name = exception_info[2] + try: + exception_info = traceback.extract_tb(tb)[-1] + filename = os.path.basename(exception_info[0]) + line_no = exception_info[1] + function_name = exception_info[2] + except IndexError: + return '', '', '' return filename, line_no, function_name def _truncate_dynamic_string(val, maxlength=500): From a905126f388bf4e6994b6b5fe544520df905acbf Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Wed, 18 Feb 2015 14:08:19 +0100 Subject: [PATCH 32/83] WebStyle: default logging level for `SERVER_RETURN` Reviewed-by: Samuele Kaplun Signed-off-by: Esteban J. G. Gabancho --- .../webstyle/lib/webinterface_handler_config.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/webstyle/lib/webinterface_handler_config.py b/modules/webstyle/lib/webinterface_handler_config.py index ca8318db0a..7d7f9bb549 100644 --- a/modules/webstyle/lib/webinterface_handler_config.py +++ b/modules/webstyle/lib/webinterface_handler_config.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # This file is part of Invenio. -# Copyright (C) 2010, 2011 CERN. +# Copyright (C) 2010, 2011, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -17,6 +17,8 @@ # along with Invenio; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +import logging + """ WebInterface (WSGI) related constants and Exceptions """ @@ -121,7 +123,16 @@ class SERVER_RETURN(Exception): - pass + + def __init__(self, code=-1): + super(SERVER_RETURN, self).__init__(code) + if code < 400: + self.level = logging.INFO + elif code >= 400 and code < 500: + self.level = logging.WARN + else: + self.level = logging.ERROR + class CookieError(Exception): pass From 72ceafa1d4750c0fb6aac577dbe34a9aa9b035c3 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Wed, 18 Feb 2015 16:34:26 +0100 Subject: [PATCH 33/83] WebStyle: logging level on some `IOError` * When the IOError happens due to a connection lost with the client the logging level is set to `INFO` as there is nothing that could be done. Reviewed-by: Samuele Kaplun Signed-off-by: Esteban J. G. Gabancho --- modules/webstyle/lib/webinterface_handler_wsgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/webstyle/lib/webinterface_handler_wsgi.py b/modules/webstyle/lib/webinterface_handler_wsgi.py index 99bd19d65f..580d595375 100644 --- a/modules/webstyle/lib/webinterface_handler_wsgi.py +++ b/modules/webstyle/lib/webinterface_handler_wsgi.py @@ -25,6 +25,7 @@ import gc import inspect import socket +import logging from fnmatch import fnmatch from urlparse import urlparse, urlunparse @@ -212,6 +213,7 @@ def flush(self): except IOError, err: if "failed to write data" in str(err) or "client connection closed" in str(err): ## Let's just log this exception without alerting the admin: + err.level=logging.INFO register_exception(req=self) self.__write_error = True ## This flag is there just ## to not report later other errors to the admin. From e3b752fdc82cc72b72e4a4c7982ee53262c09908 Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis Date: Wed, 18 Feb 2015 15:01:23 +0100 Subject: [PATCH 34/83] WebComment: remove unnecessary exceptions * Removes unnecessary `raise InvenioWebCommentWarning`, `raise InvenioWebCommentError` and `register_exception`. * Removes unnecessary messages from the `webinterface` Reviewed-by: Esteban J. G. Gabancho Signed-off-by: Harris Tzovanakis --- modules/webcomment/lib/webcomment.py | 347 +++++------------- .../webcomment/lib/webcomment_webinterface.py | 4 +- 2 files changed, 99 insertions(+), 252 deletions(-) diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index ce92ec10f0..b3f1fabbda 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -64,9 +64,7 @@ from invenio.errorlib import register_exception from invenio.messages import wash_language, gettext_set_language from invenio.urlutils import wash_url_argument -from invenio.webcomment_config import CFG_WEBCOMMENT_ACTION_CODE, \ - InvenioWebCommentError, \ - InvenioWebCommentWarning +from invenio.webcomment_config import CFG_WEBCOMMENT_ACTION_CODE from invenio.access_control_engine import acc_authorize_action from invenio.search_engine import \ guess_primary_collection_of_a_record, \ @@ -163,38 +161,14 @@ def perform_request_display_comments_or_remarks(req, recID, display_order='od', #if page <= 0 or page.lower() != 'all': if page < 0: page = 1 - try: - raise InvenioWebCommentWarning(_('Bad page number --> showing first page.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_INVALID_PAGE_NB',)) if nb_per_page < 0: nb_per_page = 100 - try: - raise InvenioWebCommentWarning(_('Bad number of results per page --> showing 10 results per page.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_INVALID_NB_RESULTS_PER_PAGE',)) if CFG_WEBCOMMENT_ALLOW_REVIEWS and reviews: if display_order not in ['od', 'nd', 'hh', 'lh', 'hs', 'ls']: display_order = 'hh' - try: - raise InvenioWebCommentWarning(_('Bad display order --> showing most helpful first.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_INVALID_REVIEW_DISPLAY_ORDER',)) else: if display_order not in ['od', 'nd']: display_order = 'od' - try: - raise InvenioWebCommentWarning(_('Bad display order --> showing oldest first.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_INVALID_DISPLAY_ORDER',)) if not display_comment_rounds: display_comment_rounds = [] @@ -207,12 +181,6 @@ def perform_request_display_comments_or_remarks(req, recID, display_order='od', last_page = 1 if page > last_page: page = 1 - try: - raise InvenioWebCommentWarning(_('Bad page number --> showing first page.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(("WRN_WEBCOMMENT_INVALID_PAGE_NB",)) if nb_res > nb_per_page: # if more than one page of results if page < last_page: res = res[(page-1)*(nb_per_page) : (page*nb_per_page)] @@ -232,65 +200,38 @@ def perform_request_display_comments_or_remarks(req, recID, display_order='od', # Send to template avg_score = 0.0 - if not CFG_WEBCOMMENT_ALLOW_COMMENTS and not CFG_WEBCOMMENT_ALLOW_REVIEWS: # comments not allowed by admin - try: - raise InvenioWebCommentError(_('Comments on records have been disallowed by the administrator.')) - except InvenioWebCommentError, exc: - register_exception(req=req) - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - # errors.append(('ERR_WEBCOMMENT_COMMENTS_NOT_ALLOWED',)) + # comments not allowed by admin + if not CFG_WEBCOMMENT_ALLOW_COMMENTS and not CFG_WEBCOMMENT_ALLOW_REVIEWS: + body = webcomment_templates.tmpl_error( + _('Comments on records have been disallowed by the' + ' administrator.'), ln) + return body if reported > 0: - try: - raise InvenioWebCommentWarning(_('Your feedback has been recorded, many thanks.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_RECORDED',)) + warnings.append((_('Your feedback has been recorded, many thanks.'), + 'green')) elif reported == 0: - try: - raise InvenioWebCommentWarning(_('You have already reported an abuse for this comment.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_ALREADY_REPORTED',)) + warnings.append((_('You have already reported an abuse for this' + ' comment.'), '')) elif reported == -2: - try: - raise InvenioWebCommentWarning(_('The comment you have reported no longer exists.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_INVALID_REPORT',)) + warnings.append((_('The comment you have reported no longer ' + 'exists.'), '')) if CFG_WEBCOMMENT_ALLOW_REVIEWS and reviews: avg_score = calculate_avg_score(res) if voted > 0: - try: - raise InvenioWebCommentWarning(_('Your feedback has been recorded, many thanks.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_RECORDED',)) + warnings.append((_('Your feedback has been recorded, many' + ' thanks.'), 'green')) elif voted == 0: - try: - raise InvenioWebCommentWarning(_('Sorry, you have already voted. This vote has not been recorded.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_ALREADY_VOTED',)) + warnings.append((_('Sorry, you have already voted. This vote has ' + 'not been recorded.'), '')) if subscribed == 1: - try: - raise InvenioWebCommentWarning(_('You have been subscribed to this discussion. From now on, you will receive an email whenever a new comment is posted.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_SUBSCRIBED',)) + warnings.append( + (_('You have been subscribed to this discussion. From now on, you' + ' will receive an email whenever a new comment is posted.'), + 'green') + ) elif subscribed == -1: - try: - raise InvenioWebCommentWarning(_('You have been unsubscribed from this discussion.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_UNSUBSCRIBED',)) + warnings.append((_('You have been unsubscribed from this discussion.'), + 'green')) grouped_comments = group_comments_by_round(res, reviews) @@ -1419,53 +1360,35 @@ def get_first_comments_or_remarks(recID=-1, first_res_comments = res_comments[:nb_comments] else: first_res_comments = res_comments - else: #error - try: - raise InvenioWebCommentError(_('%s is an invalid record ID') % recID) - except InvenioWebCommentError, exc: - register_exception() - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_RECID_INVALID', recID)) #!FIXME dont return error anywhere since search page + else: + body = webcomment_templates.tmpl_error( + _('%s is an invalid record ID') % recID, ln) + return body # comment if recID >= 1: comments = reviews = "" if reported > 0: - try: - raise InvenioWebCommentWarning(_('Your feedback has been recorded, many thanks.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_RECORDED_GREEN_TEXT',)) + warnings.append((_('Your feedback has been recorded, many ' + 'thanks.'), 'green')) elif reported == 0: - try: - raise InvenioWebCommentWarning(_('Your feedback could not be recorded, please try again.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_NOT_RECORDED_RED_TEXT',)) - if CFG_WEBCOMMENT_ALLOW_COMMENTS: # normal comments + warnings.append((_('Your feedback could not be recorded, please' + ' try again.'), '')) + # normal comments + if CFG_WEBCOMMENT_ALLOW_COMMENTS: grouped_comments = group_comments_by_round(first_res_comments, ranking=0) comments = webcomment_templates.tmpl_get_first_comments_without_ranking(recID, ln, grouped_comments, nb_res_comments, warnings) if show_reviews: - if CFG_WEBCOMMENT_ALLOW_REVIEWS: # ranked comments - #calculate average score + # ranked comments + if CFG_WEBCOMMENT_ALLOW_REVIEWS: + # calculate average score avg_score = calculate_avg_score(res_reviews) if voted > 0: - try: - raise InvenioWebCommentWarning(_('Your feedback has been recorded, many thanks.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, 'green')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_RECORDED_GREEN_TEXT',)) + warnings.append((_('Your feedback has been recorded, ' + 'many thanks.'), 'green')) elif voted == 0: - try: - raise InvenioWebCommentWarning(_('Your feedback could not be recorded, please try again.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_FEEDBACK_NOT_RECORDED_RED_TEXT',)) + warnings.append((_('Your feedback could not be recorded, ' + 'please try again.'), '')) grouped_reviews = group_comments_by_round(first_res_reviews, ranking=0) reviews = webcomment_templates.tmpl_get_first_comments_with_ranking(recID, ln, grouped_reviews, nb_res_reviews, avg_score, warnings) return (comments, reviews) @@ -1568,14 +1491,9 @@ def perform_request_add_comment_or_remark(recID=0, ## check arguments check_recID_is_in_range(recID, warnings, ln) if uid <= 0: - try: - raise InvenioWebCommentError(_('%s is an invalid user ID.') % uid) - except InvenioWebCommentError, exc: - register_exception() - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_UID_INVALID', uid)) - return '' + body = webcomment_templates.tmpl_error( + _('%s is an invalid user ID.') % uid, ln) + return body if attached_files is None: attached_files = {} @@ -1592,24 +1510,16 @@ def perform_request_add_comment_or_remark(recID=0, elif not reviews and CFG_WEBCOMMENT_ALLOW_COMMENTS: return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, can_attach_files=can_attach_files) else: - try: - raise InvenioWebCommentError(_('Comments on records have been disallowed by the administrator.')) - except InvenioWebCommentError, exc: - register_exception(req=req) - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_COMMENTS_NOT_ALLOWED',)) + body = webcomment_templates.tmpl_error( + _('Comments on records have been disallowed by the ' + 'administrator.'), ln) + return body elif action == 'REPLY': if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: - try: - raise InvenioWebCommentError(_('Cannot reply to a review.')) - except InvenioWebCommentError, exc: - register_exception(req=req) - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_REPLY_REVIEW',)) - return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, nickname, ln, msg, score, note, warnings, can_attach_files=can_attach_files) + body = webcomment_templates.tmpl_error( + _('Cannot reply to a review.'), ln) + return body elif not reviews and CFG_WEBCOMMENT_ALLOW_COMMENTS: textual_msg = msg if comID > 0: @@ -1645,38 +1555,20 @@ def perform_request_add_comment_or_remark(recID=0, textual_msg = email_quote_txt(text=textual_msg) return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, textual_msg, can_attach_files=can_attach_files, reply_to=comID) else: - try: - raise InvenioWebCommentError(_('Comments on records have been disallowed by the administrator.')) - except InvenioWebCommentError, exc: - register_exception(req=req) - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_COMMENTS_NOT_ALLOWED',)) + body = webcomment_templates.tmpl_error( + _('Comments on records have been disallowed by the ' + 'administrator.'), ln) + return body # check before submitting form elif action == 'SUBMIT': if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: if note.strip() in ["", "None"] and not CFG_WEBCOMMENT_ALLOW_SHORT_REVIEWS: - try: - raise InvenioWebCommentWarning(_('You must enter a title.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_ADD_NO_TITLE',)) + warnings.append((_('You must enter a title.'), '')) if score == 0 or score > 5: - try: - raise InvenioWebCommentWarning(_('You must choose a score.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(("WRN_WEBCOMMENT_ADD_NO_SCORE",)) + warnings.append((_('You must choose a score.'), '')) if msg.strip() in ["", "None"] and not CFG_WEBCOMMENT_ALLOW_SHORT_REVIEWS: - try: - raise InvenioWebCommentWarning(_('You must enter a text.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_ADD_NO_BODY',)) + warnings.append((_('You must enter a text.'), '')) # if no warnings, submit if len(warnings) == 0: if reviews: @@ -1688,12 +1580,8 @@ def perform_request_add_comment_or_remark(recID=0, req=req, reply_to=comID) else: - try: - raise InvenioWebCommentWarning(_('You already wrote a review for this record.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append('WRN_WEBCOMMENT_CANNOT_REVIEW_TWICE') + warnings.append((_('You already wrote a review for ' + 'this record.'), '')) success = 1 else: if check_user_can_comment(recID, client_ip_address, uid): @@ -1707,25 +1595,19 @@ def perform_request_add_comment_or_remark(recID=0, if success > 0 and subscribe: subscribe_user_to_discussion(recID, uid) else: - try: - raise InvenioWebCommentWarning(_('You already posted a comment short ago. Please retry later.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append('WRN_WEBCOMMENT_TIMELIMIT') + warnings.append((_('You already posted a comment ' + 'short ago. Please retry later.'), '')) success = 1 if success > 0: if CFG_WEBCOMMENT_ADMIN_NOTIFICATION_LEVEL > 0: notify_admin_of_new_comment(comID=success) return webcomment_templates.tmpl_add_comment_successful(recID, ln, reviews, warnings, success) else: - try: - raise InvenioWebCommentError(_('Failed to insert your comment to the database. Please try again.')) - except InvenioWebCommentError, exc: - register_exception(req=req) - body = webcomment_templates.tmpl_error(exc.message, ln) - return body - #errors.append(('ERR_WEBCOMMENT_DB_INSERT_ERROR')) + register_exception(req=req) + body = webcomment_templates.tmpl_error( + _('Failed to insert your comment to the database.' + ' Please try again.'), ln) + return body # if are warnings or if inserting comment failed, show user where warnings are if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, nickname, ln, msg, score, note, warnings, can_attach_files=can_attach_files) @@ -1733,12 +1615,8 @@ def perform_request_add_comment_or_remark(recID=0, return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, can_attach_files=can_attach_files) # unknown action send to display else: - try: - raise InvenioWebCommentWarning(_('Unknown action --> showing you the default add comment form.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning', req=req) - warnings.append((exc.message, '')) - #warnings.append(('WRN_WEBCOMMENT_ADD_UNKNOWN_ACTION',)) + warnings.append((_('Unknown action --> showing you the default ' + 'add comment form.'), '')) if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, ln, msg, score, note, warnings, can_attach_files=can_attach_files) else: @@ -1853,48 +1731,34 @@ def check_recID_is_in_range(recID, warnings=[], ln=CFG_SITE_LANG): from invenio.search_engine import record_exists success = record_exists(recID) if success == 1: - return (1,"") + return (1, "") else: - try: - if success == -1: - status = 'deleted' - raise InvenioWebCommentWarning(_('The record has been deleted.')) - else: - status = 'inexistant' - raise InvenioWebCommentWarning(_('Record ID %s does not exist in the database.') % recID) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('ERR_WEBCOMMENT_RECID_INEXISTANT', recID)) - return (0, webcomment_templates.tmpl_record_not_found(status=status, recID=recID, ln=ln)) + if success == -1: + status = 'deleted' + warning_message = _('The record has been deleted.') + else: + status = 'inexistant' + warning_message = _( + 'Record ID %s does not exist in the database.') % recID + warnings.append((warning_message, '')) + return (0, webcomment_templates.tmpl_record_not_found( + status=status, recID=recID, ln=ln)) elif recID == 0: - try: - raise InvenioWebCommentWarning(_('No record ID was given.')) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('ERR_WEBCOMMENT_RECID_MISSING',)) - return (0, webcomment_templates.tmpl_record_not_found(status='missing', recID=recID, ln=ln)) + warnings.append((_('No record ID was given.'), '')) + return (0, webcomment_templates.tmpl_record_not_found( + status='missing', recID=recID, ln=ln)) else: - try: - raise InvenioWebCommentWarning(_('Record ID %s is an invalid ID.') % recID) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('ERR_WEBCOMMENT_RECID_INVALID', recID)) - return (0, webcomment_templates.tmpl_record_not_found(status='invalid', recID=recID, ln=ln)) + warnings.append((_('Record ID %s is an invalid ID.') % recID, '')) + return (0, webcomment_templates.tmpl_record_not_found( + status='invalid', recID=recID, ln=ln)) else: - try: - raise InvenioWebCommentWarning(_('Record ID %s is not a number.') % recID) - except InvenioWebCommentWarning, exc: - register_exception(stream='warning') - warnings.append((exc.message, '')) - #warnings.append(('ERR_WEBCOMMENT_RECID_NAN', recID)) - return (0, webcomment_templates.tmpl_record_not_found(status='nan', recID=recID, ln=ln)) + warnings.append((_('Record ID %s is not a number.') % recID, '')) + return (0, webcomment_templates.tmpl_record_not_found( + status='nan', recID=recID, ln=ln)) def check_int_arg_is_in_range(value, name, gte_value, lte_value=None): """ - Check that variable with name 'name' >= gte_value and optionally <= lte_value + Check that variable with name 'name' >= gte_value & optionally <= lte_value @param value: variable value @param name: variable name @param errors: list of error tuples (error_id, value) @@ -1904,34 +1768,17 @@ def check_int_arg_is_in_range(value, name, gte_value, lte_value=None): """ if type(value) is not int: - try: - raise InvenioWebCommentError('%s is not a number.' % value) - except InvenioWebCommentError, exc: - register_exception() - body = webcomment_templates.tmpl_error(exc.message) - return body - #errors.append(('ERR_WEBCOMMENT_ARGUMENT_NAN', value)) - return 0 + body = webcomment_templates.tmpl_error('%s is not a number.' % value) + return body if value < gte_value: - try: - raise InvenioWebCommentError('%s invalid argument.' % value) - except InvenioWebCommentError, exc: - register_exception() - body = webcomment_templates.tmpl_error(exc.message) - return body - #errors.append(('ERR_WEBCOMMENT_ARGUMENT_INVALID', value)) - return 0 + body = webcomment_templates.tmpl_error('%s invalid argument.' % value) + return body if lte_value: if value > lte_value: - try: - raise InvenioWebCommentError('%s invalid argument.' % value) - except InvenioWebCommentError, exc: - register_exception() - body = webcomment_templates.tmpl_error(exc.message) - return body - #errors.append(('ERR_WEBCOMMENT_ARGUMENT_INVALID', value)) - return 0 + body = webcomment_templates.tmpl_error( + '%s invalid argument.' % value) + return body return 1 def get_mini_reviews(recid, ln=CFG_SITE_LANG): diff --git a/modules/webcomment/lib/webcomment_webinterface.py b/modules/webcomment/lib/webcomment_webinterface.py index 000117b7a3..469d5e744c 100644 --- a/modules/webcomment/lib/webcomment_webinterface.py +++ b/modules/webcomment/lib/webcomment_webinterface.py @@ -710,7 +710,7 @@ def subscribe(self, req, form): success = subscribe_user_to_discussion(self.recid, uid) display_url = "%s/%s/%s/comments/display?subscribed=%s&ln=%s" % \ (CFG_SITE_SECURE_URL, CFG_SITE_RECORD, self.recid, str(success), argd['ln']) - redirect_to_url(req, display_url) + return redirect_to_url(req, display_url) def unsubscribe(self, req, form): """ @@ -731,7 +731,7 @@ def unsubscribe(self, req, form): success = unsubscribe_user_from_discussion(self.recid, uid) display_url = "%s/%s/%s/comments/display?subscribed=%s&ln=%s" % \ (CFG_SITE_SECURE_URL, CFG_SITE_RECORD, self.recid, str(-success), argd['ln']) - redirect_to_url(req, display_url) + return redirect_to_url(req, display_url) def toggle(self, req, form): """ From 5f224b0366a5e4bdfb8919bd7972541991009d42 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Thu, 19 Feb 2015 14:37:29 +0100 Subject: [PATCH 35/83] WebSubmit: fix for Stamp_Uploaded_Files * Fixes the stamping function by checking if the object that will be stamped is a file, not a directory. Signed-off-by: Sebastian Witowski --- modules/websubmit/lib/functions/Stamp_Uploaded_Files.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/websubmit/lib/functions/Stamp_Uploaded_Files.py b/modules/websubmit/lib/functions/Stamp_Uploaded_Files.py index e8b5b42147..40c9cee7e6 100644 --- a/modules/websubmit/lib/functions/Stamp_Uploaded_Files.py +++ b/modules/websubmit/lib/functions/Stamp_Uploaded_Files.py @@ -302,8 +302,8 @@ def visit_for_stamping(visit_for_stamping_arguments, dirname, filenames): 'file_stamper_options' members. @param dirname: (string) - the path to the directory in which the files are to be stamped. - @param filenames: (list) - the names of each file in dirname. An - attempt will be made to stamp each of these files. + @param filenames: (list) - the names of each file and subdirectory in + dirname. An attempt will be made to stamp each of the files. @Exceptions Raised: + InvenioWebSubmitFunctionWarning; + InvenioWebSubmitFunctionError; @@ -345,6 +345,10 @@ def visit_for_stamping(visit_for_stamping_arguments, dirname, filenames): path_to_subject_file = "%s/%s" % (dirname, file_to_stamp) file_stamper_options['input-file'] = path_to_subject_file + if not os.path.isfile(path_to_subject_file): + # If it's not a file, we can't stamp it. Continue with next file + continue + ## Just before attempting to stamp the file, log the dictionary of ## options (file_stamper_options) that will be passed to websubmit- ## file-stamper: From 83b1252d40d4d1693adcc86719fa5e035d3d516e Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Mon, 23 Feb 2015 15:40:38 +0100 Subject: [PATCH 36/83] BibFormat: fix for missing cache * FIX Changes when the missing caches are generated during bibformat run. The missing cache is no longer generated for records if the bibreformat is run with -i, --collection, --field or --pattern option. Signed-off-by: Sebastian Witowski Reviewed-by: Samuele Kaplun --- modules/bibformat/lib/bibreformat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/bibformat/lib/bibreformat.py b/modules/bibformat/lib/bibreformat.py index 1571146adb..9c712dcbd3 100644 --- a/modules/bibformat/lib/bibreformat.py +++ b/modules/bibformat/lib/bibreformat.py @@ -353,7 +353,11 @@ def task_run_core(): if task_has_option("last"): recids += outdated_caches(fmt, last_updated) - if task_has_option('ignore_without'): + if task_has_option('ignore_without') or \ + task_has_option('collection') or \ + task_has_option('field') or \ + task_has_option('pattern') or \ + task_has_option('recids'): without_fmt = intbitset() else: without_fmt = missing_caches(fmt) From c9b7e098db5dc9a976c5178f1c80387e45e3fed9 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Fri, 25 Oct 2013 11:32:09 +0200 Subject: [PATCH 37/83] BibDocFile: add download all files in BibRecDoc * Modifies the getfile() function from WebInterfaceFilesPages class, so if the filename starts with "allfiles-", tar archive is created and send to the user. * Allows user to download only those files that are not restricted * Adds the stream_archive_of_latest_files(), that creates the archive with the latest files of a specific format (original or subformat) and streams this archive to the user. * Adds the subformat parameter to the get_total_size_latest_version(). If that parameter is given, function limits result to this subformat. * Adds CFG_BIBDOCFILE_SUBFORMATS_TRANSLATIONS to the bibdocfile_config. * (closes #1673) Signed-off-by: Sebastian Witowski --- modules/bibdocfile/lib/bibdocfile.py | 61 +++++++++++++++++-- modules/bibdocfile/lib/bibdocfile_config.py | 9 +++ .../bibdocfile/lib/bibdocfile_webinterface.py | 6 ++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index 12598b36ac..71e99f19eb 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -59,6 +59,7 @@ import cgi import sys import copy +import tarfile if sys.hexversion < 0x2060000: from md5 import md5 @@ -102,6 +103,7 @@ encode_for_xml from invenio.urlutils import create_url, make_user_agent_string from invenio.textutils import nice_size +from invenio.webuser import collect_user_info from invenio.access_control_engine import acc_authorize_action from invenio.access_control_admin import acc_is_user_in_role, acc_get_role_id from invenio.access_control_firerole import compile_role_definition, acc_firerole_check_user @@ -123,7 +125,7 @@ CFG_BIBCATALOG_SYSTEM from invenio.bibcatalog import BIBCATALOG_SYSTEM from invenio.bibdocfile_config import CFG_BIBDOCFILE_ICON_SUBFORMAT_RE, \ - CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT + CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT, CFG_BIBDOCFILE_SUBFORMATS_TRANSLATIONS from invenio.pluginutils import PluginContainer import invenio.template @@ -797,17 +799,22 @@ def get_xml_8564(self): return out - def get_total_size_latest_version(self): + def get_total_size_latest_version(self, user_info=None, subformat=None): """ Returns the total size used on disk by all the files belonging to this record and corresponding to the latest version. + @param user_info: the user_info dictionary, used to check restrictions + @type: dict + @param subformat: if subformat is specified, it limits files + only to those from that specific subformat + @type subformat: string @return: the total size. @rtype: integer """ size = 0 for (bibdoc, _) in self.bibdocs.values(): - size += bibdoc.get_total_size_latest_version() + size += bibdoc.get_total_size_latest_version(user_info, subformat) return size def get_total_size(self): @@ -1564,6 +1571,34 @@ def get_text(self, extract_text_if_necessary=True): return " ".join(texts) + def stream_archive_of_latest_files(self, req, format=''): + """ + Streams the tar archive with all files of a certain format (that are + not restricted) to the user. + Formats should be strings that can be compared with the output of + BibDocFile.get_subformat() function. + + @param req: Apache Request Object + @type req: Apache Request Object + @param format: format of the files. Empty string means the original format. + @type format: string + """ + # Get the internal size from the user-friendly format name + internal_format = [f[1] for f in CFG_BIBDOCFILE_SUBFORMATS_TRANSLATIONS if f[0] == format] + if len(internal_format) < 1: + # Incorrect format + return + internal_format = internal_format[0] + tarname = str(self.id) + "_" + format + '.tar' + # select only those files that user can download + user_info = collect_user_info(req) + req.content_type = "application/x-tar" + req.headers_out["Content-Disposition"] = 'attachment; filename="%s"' % tarname + tar = tarfile.open(fileobj=req, mode='w|') + for f in self.list_latest_files(): + if f.get_subformat() == internal_format and f.is_restricted(user_info)[0] == 0: + tar.add(f.get_path(), arcname=f.get_full_name(), recursive=False) + tar.close() class BibDoc(object): """ @@ -2801,12 +2836,26 @@ def _build_related_file_list(self): cur_doc = BibDoc.create_instance(docid=docid, human_readable=self.human_readable) self.related_files[doctype].append(cur_doc) - def get_total_size_latest_version(self): + def get_total_size_latest_version(self, user_info=None, subformat=None): """Return the total size used on disk of all the files belonging - to this bibdoc and corresponding to the latest version.""" + to this bibdoc and corresponding to the latest version. Restricted + files are not counted. + @param user_info: the user_info dictionary, used to check restrictions + @type: dict + @param subformat: if subformat is specified, it limits files + only to those from that specific subformat + @type subformat: string + """ ret = 0 + all_files = False + # If we are calling this function without user_info, then we want to + # see all the files + if not user_info: + all_files = True for bibdocfile in self.list_latest_files(): - ret += bibdocfile.get_size() + if (all_files or bibdocfile.is_restricted(user_info)[0] == 0) and \ + (subformat is None or bibdocfile.get_subformat() == subformat): + ret += bibdocfile.get_size() return ret def get_total_size(self): diff --git a/modules/bibdocfile/lib/bibdocfile_config.py b/modules/bibdocfile/lib/bibdocfile_config.py index 0caa8ddbd2..e73193f2ab 100644 --- a/modules/bibdocfile/lib/bibdocfile_config.py +++ b/modules/bibdocfile/lib/bibdocfile_config.py @@ -69,3 +69,12 @@ # CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT -- this is the default subformat used # when creating new icons. CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT = "icon" + +# CFG_BIBDOCFILE_SUBFORMATS_TRANSLATIONS -- a list (not dictionary, because +# we want to preserve the order) that connects the different format sizes +# (like 'small', 'medium', etc.) with internal format sizes (like 'icon-180', 'icon-640', etc.) +CFG_BIBDOCFILE_SUBFORMATS_TRANSLATIONS = [ + ('small', 'icon-180'), + ('medium', 'icon-640'), + ('large', 'icon-1440'), + ('original', '')] diff --git a/modules/bibdocfile/lib/bibdocfile_webinterface.py b/modules/bibdocfile/lib/bibdocfile_webinterface.py index 1547dd3ddf..fe9c0eb74b 100644 --- a/modules/bibdocfile/lib/bibdocfile_webinterface.py +++ b/modules/bibdocfile/lib/bibdocfile_webinterface.py @@ -83,6 +83,12 @@ def _lookup(self, component, path): def getfile(req, form): args = wash_urlargd(form, bibdocfile_templates.files_default_urlargd) ln = args['ln'] + if filename[:9] == "allfiles-": + filesizes = filename[9:] + # stream a tar package to the user + brd = BibRecDocs(self.recid) + brd.stream_archive_of_latest_files(req, filesizes) + return _ = gettext_set_language(ln) From 5897dfa5775824e37780a74f086645a7bbf08afa Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" Date: Thu, 26 Feb 2015 09:05:09 +0100 Subject: [PATCH 38/83] BibField: bug fixes --- modules/bibfield/etc/atlantis.cfg | 16 ++++-- modules/bibfield/lib/bibfield.py | 4 +- .../bibfield/lib/bibfield_config_engine.py | 56 ++++++++++++------- .../lib/bibfield_marcreader_unit_tests.py | 4 +- .../lib/functions/produce_json_for_marc.py | 11 ++-- 5 files changed, 58 insertions(+), 33 deletions(-) diff --git a/modules/bibfield/etc/atlantis.cfg b/modules/bibfield/etc/atlantis.cfg index 8e170656e4..217a13e17a 100644 --- a/modules/bibfield/etc/atlantis.cfg +++ b/modules/bibfield/etc/atlantis.cfg @@ -30,9 +30,9 @@ abstract: ("520__a", "abstract", "summary"), ("520__b", "expansion"), ("520__9", "number")) - marc, "520__", {'summary':value['a'], 'expansion':value['b'], 'number':value['9']} + marc, "520__", {'summary':value['a'], 'expansion':value['b'], 'number':value['9'], 'source': value['2']} producer: - json_for_marc(), {"520__a": "summary", "520__b": "expansion", "520__9": "number"} + json_for_marc(), {"520__a": "summary", "520__b": "expansion", "520__9": "number", "520__2": "source"} json_for_dc(), {"dc:description":"summary"} abstract_french: @@ -123,6 +123,8 @@ aleph_linking_page: json_for_marc(), {"962__a":"type", "962__b":"sysno", "962__l":"library", "962__n":"down_link", "962__m":"up_link", "962__y":"volume_link", "962__p":"part_link", "962__i":"issue_link", "962__k":"pages", "962__t":"base"} authors[0], creator: + schema: + {'authors[0]': {'default': lambda: dict(full_name=None)}} creator: @legacy((("100", "100__", "100__%"), ""), ("100__a", "first author name", "full_name"), @@ -383,7 +385,9 @@ experiment: ('909C0e', 'experiment', '')) marc, "909C0", value['e'] -fft[n]: +fft: + schema: + {'fft': {'type': list, 'force': True}} creator: @legacy(("FFT__a", "path"), ("FFT__d", "description"), @@ -528,7 +532,9 @@ journal_info: json_for_marc(), {"909C4a": "doi","909C4c": "pagination", "909C4d": "date", "909C4e": "recid", "909C4f": "note", "909C4n": "number", "909C4p": "title", "909C4u": "url","909C4v": "volume", "909C4y": "year", "909C4t": "talk", "909C4w": "cnum", "909C4x": "reference"} -keywords[n]: +keywords: + schema: + {'keywords': {'type': list, 'force': True}} creator: @legacy((("653", "6531_", "6531_%"), ""), ("6531_a", "keyword", "term"), @@ -876,7 +882,7 @@ system_number: checker: check_field_existence(0,1) producer: - json_for_marc(), {"970__a": "sysno", "970__d": "recid"} + json_for_marc(), {"970__a": "value", "970__d": "recid"} thesaurus_terms: creator: diff --git a/modules/bibfield/lib/bibfield.py b/modules/bibfield/lib/bibfield.py index 632eb2c498..630613ba89 100644 --- a/modules/bibfield/lib/bibfield.py +++ b/modules/bibfield/lib/bibfield.py @@ -85,7 +85,9 @@ def create_records(blob, master_format='marc', verbose=0, **additional_info): """ record_blods = CFG_BIBFIELD_READERS['bibfield_%sreader.py' % (master_format,)].split_blob(blob, additional_info.get('schema', None)) - return [create_record(record_blob, master_format, verbose=verbose, **additional_info) for record_blob in record_blods] + for record_blob in record_blods: + yield create_record( + record_blob, master_format, verbose=verbose, **additional_info) def get_record(recid, reset_cache=False): diff --git a/modules/bibfield/lib/bibfield_config_engine.py b/modules/bibfield/lib/bibfield_config_engine.py index 3eaf4a2188..4ed2045dd9 100644 --- a/modules/bibfield/lib/bibfield_config_engine.py +++ b/modules/bibfield/lib/bibfield_config_engine.py @@ -147,10 +147,12 @@ def do_unindent(): inherit_from = (Suppress("@inherit_from") + \ originalTextFor(nestedExpr("(", ")")))\ .setResultsName("inherit_from") - override = (Suppress("@") + "override")\ - .setResultsName("override") - extend = (Suppress("@") + "extend")\ - .setResultsName("extend") + override = Suppress("@override") \ + .setResultsName("override") \ + .setParseAction(lambda toks: True) + extend = Suppress("@extend") \ + .setResultsName("extend") \ + .setParseAction(lambda toks: True) master_format = (Suppress("@master_format") + \ originalTextFor(nestedExpr("(", ")")))\ .setResultsName("master_format") \ @@ -298,9 +300,13 @@ def _create(self): to fill up config_rules """ parser = _create_field_parser() - main_rules = parser \ - .parseFile(self.base_dir + '/' + self.main_config_file, - parseAll=True) + try: + main_rules = parser \ + .parseFile(self.base_dir + '/' + self.main_config_file, + parseAll=True) + except ParseException as e: + raise BibFieldParserException( + "Cannot parse file '%s',\n%s" % (self.main_config_file, str(e))) rules = main_rules.rules includes = main_rules.includes already_includes = [self.main_config_file] @@ -310,20 +316,23 @@ def _create(self): if include[0] in already_includes: continue already_includes.append(include[0]) - if os.path.exists(include[0]): - tmp = parser.parseFile(include[0], parseAll=True) - else: - #CHECK: This will raise an IOError if the file doesn't exist - tmp = parser.parseFile(self.base_dir + '/' + include[0], - parseAll=True) + try: + if os.path.exists(include[0]): + tmp = parser.parseFile(include[0], parseAll=True) + else: + #CHECK: This will raise an IOError if the file doesn't exist + tmp = parser.parseFile(self.base_dir + '/' + include[0], + parseAll=True) + except ParseException as e: + raise BibFieldParserException( + "Cannot parse file '%s',\n%s" % (include[0], str(e))) if rules and tmp.rules: rules += tmp.rules else: rules = tmp.rules - if includes and tmp.includes: + + if tmp.includes: includes += tmp.includes - else: - includes = tmp.includes #Create config rules for rule in rules: @@ -378,14 +387,14 @@ def _create_rule(self, rule, override=False, extend=False): % (rule.json_id[0],)) if not json_id in self.__class__._field_definitions and (override or extend): raise BibFieldParserException("Name error: '%s' field name not defined" - % (rule.json_id[0],)) + % (rule.json_id[0],)) #Workaround to keep clean doctype files #Just creates a dict entry with the main json field name and points it to #the full one i.e.: 'authors' : ['authors[0]', 'authors[n]'] - if '[0]' in json_id or '[n]' in json_id: + if ('[0]' in json_id or '[n]' in json_id) and not (extend or override): main_json_id = re.sub('(\[n\]|\[0\])', '', json_id) - if not main_json_id in self.__class__._field_definitions: + if main_json_id not in self.__class__._field_definitions: self.__class__._field_definitions[main_json_id] = [] self.__class__._field_definitions[main_json_id].append(json_id) @@ -526,7 +535,8 @@ def __create_description(self, rule): def __create_producer(self, rule): json_id = rule.json_id[0] - producers = dict() + producers = dict() if not rule.extend else \ + self.__class__._field_definitions[json_id].get('producer', {}) for producer in rule.producer_rule: if producer.producer_code[0][0] not in producers: producers[producer.producer_code[0][0]] = [] @@ -535,8 +545,12 @@ def __create_producer(self, rule): self.__class__._field_definitions[json_id]['producer'] = producers def __create_schema(self, rule): + from invenio.bibfield_utils import CFG_BIBFIELD_FUNCTIONS json_id = rule.json_id[0] - self.__class__._field_definitions[json_id]['schema'] = rule.schema if rule.schema else {} + self.__class__._field_definitions[json_id]['schema'] = \ + try_to_eval(rule.schema.strip(), CFG_BIBFIELD_FUNCTIONS) \ + if rule.schema \ + else self.__class__._field_definitions[json_id].get('schema', {}) def __create_json_extra(self, rule): from invenio.bibfield_utils import CFG_BIBFIELD_FUNCTIONS diff --git a/modules/bibfield/lib/bibfield_marcreader_unit_tests.py b/modules/bibfield/lib/bibfield_marcreader_unit_tests.py index aae14383b2..e690bd9e4b 100644 --- a/modules/bibfield/lib/bibfield_marcreader_unit_tests.py +++ b/modules/bibfield/lib/bibfield_marcreader_unit_tests.py @@ -529,13 +529,13 @@ def test_check_error_reporting(self): reader = MarcReader(blob=xml, schema="xml") r = Record(reader.translate()) - r.check_record(reset = True) + r.check_record(reset=True) self.assertTrue('title' in r) self.assertEquals(len(r['title']), 2) self.assertEquals(len(r.fatal_errors), 1) r['title'] = r['title'][0] - r.check_record(reset = True) + r.check_record(reset=True) self.assertEquals(len(r.fatal_errors), 0) TEST_SUITE = make_test_suite(BibFieldMarcReaderMarcXML, diff --git a/modules/bibfield/lib/functions/produce_json_for_marc.py b/modules/bibfield/lib/functions/produce_json_for_marc.py index f006b4a37a..6d5ec75002 100644 --- a/modules/bibfield/lib/functions/produce_json_for_marc.py +++ b/modules/bibfield/lib/functions/produce_json_for_marc.py @@ -24,6 +24,7 @@ def produce_json_for_marc(self, fields=None): @param tags: list of tags to include in the output, if None or empty list all available tags will be included. """ + from invenio.importutils import try_to_eval from invenio.bibfield_config_engine import get_producer_rules if not fields: fields = self.keys() @@ -50,14 +51,16 @@ def produce_json_for_marc(self, fields=None): tmp_dict[key] = f else: try: - tmp_dict[key] = f[subfield] + tmp_dict[key] = f.get(subfield) except: try: - tmp_dict[key] = self._try_to_eval(subfield, value=f) + tmp_dict[key] = try_to_eval(subfield, self=self, value=f) except Exception as e: - self['__error_messages.cerror[n]'] = 'Producer CError - Unable to produce %s - %s' % (field, str(e)) + self.continuable_errors.append( + 'Producer CError - Unable to produce %s - %s' % (field, str(e))) if tmp_dict: out.append(tmp_dict) except KeyError: - self['__error_messages.cerror[n]'] = 'Producer CError - No producer rule for field %s' % field + self.continuable_errors.append( + 'Producer CError - No producer rule for field %s' % field) return out From ea1d594fdb84bd9aeca5210a0aa0c34bdecf373d Mon Sep 17 00:00:00 2001 From: Sebastian Witowski Date: Mon, 9 Mar 2015 10:22:53 +0100 Subject: [PATCH 39/83] BibReformat: new --only-missing option * Adds a new option to bibreformat that will filter all the records that are supposed to be formatted and only format those, that don't have cache. Signed-off-by: Sebastian Witowski --- modules/bibformat/lib/bibreformat.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/bibformat/lib/bibreformat.py b/modules/bibformat/lib/bibreformat.py index 1571146adb..5ba1bc34c9 100644 --- a/modules/bibformat/lib/bibreformat.py +++ b/modules/bibformat/lib/bibreformat.py @@ -368,6 +368,12 @@ def task_run_core(): 'matching': task_get_option('matching', '')} recids += query_records(query_params) + if task_has_option("only_missing"): + # From all the recIDs that we have collected so far, we want to + # reformat only those that don't have cache + without_fmt = missing_caches(fmt) + recids = recids.intersection(without_fmt) + bibreformat_task(fmt, recids, without_fmt, @@ -425,6 +431,7 @@ def main(): -p, --pattern \t Force reformatting records by pattern -i, --id \t Force reformatting records by record id(s) --no-missing \t Ignore reformatting records without format + --only-missing \t Reformatting only the records without formats Pattern options: -m, --matching \t Specify if pattern is exact (e), regular expression (r), \t partial (p), any of the words (o) or all of the words (a) @@ -439,6 +446,7 @@ def main(): "format=", "noprocess", "id=", + "only-missing", "no-missing"]), task_submit_check_options_fnc=task_submit_check_options, task_submit_elaborate_specific_parameter_fnc= @@ -467,6 +475,8 @@ def task_submit_elaborate_specific_parameter(key, value, opts, args): # pylint: task_set_option("all", 1) elif key in ("--no-missing", ): task_set_option("ignore_without", 1) + elif key in ("--only-missing", ): + task_set_option("only_missing", 1) elif key in ("-c", "--collection"): task_set_option("collection", value) elif key in ("-n", "--noprocess"): From feca70daec2d1d06d55a52eccbe40e4ec1fdcf50 Mon Sep 17 00:00:00 2001 From: Avraam Tsantekidis Date: Mon, 10 Mar 2014 15:10:55 +0100 Subject: [PATCH 40/83] WebSubmit: autocompeltion for authors * FEATURE Provides an author autocompletion facility for WebSubmit. (closes #2276) * The author autocompletion interface can easily be configured for each submission. * Author sources can be defined as plugins that return results in JSON. * Author submission and modification are fully compatible with existing BibConvert templates. * Uses typeahead.js for autocompletion. Co-Authored-By: Harris Tzovanakis Co-Authored-By: Esteban J. G. Gabancho Co-authored-by: Nikolaos Kasioumis Co-authored-by: Sebastian Witowski Signed-off-by: Nikolaos Kasioumis --- Makefile.am | 8 + configure.ac | 1 + modules/bibfield/lib/bibfield_utils.py | 18 + modules/miscutil/demo/democfgdata.sql | 27 +- ...nvenio_2014_10_09_author_autocompletion.py | 77 ++++ modules/miscutil/sql/tabfill.sql | 1 + modules/webstyle/css/Makefile.am | 6 +- .../webstyle/css/author_autocompletion.css | 204 +++++++++ modules/websubmit/etc/DEMOTHE.tpl | 5 + modules/websubmit/etc/DEMOTHEcreate.tpl | 4 +- modules/websubmit/etc/DEMOTHEmodify.tpl | 4 +- modules/websubmit/lib/Makefile.am | 7 +- .../websubmit/lib/author_sources/Makefile.am | 25 ++ .../websubmit/lib/author_sources/__init__.py | 0 .../lib/author_sources/bibauthority.py | 54 +++ .../lib/functions/Create_Modify_Interface.py | 7 +- modules/websubmit/lib/functions/Makefile.am | 1 + .../lib/functions/process_authors_json.py | 119 +++++ modules/websubmit/lib/websubmit_config.py | 56 ++- modules/websubmit/lib/websubmit_engine.py | 255 ++++++++++- modules/websubmit/lib/websubmit_templates.py | 405 +++++++++++++++++- modules/websubmit/lib/websubmit_unit_tests.py | 40 ++ modules/websubmit/lib/websubmit_web_tests.py | 77 +++- .../websubmit/lib/websubmit_webinterface.py | 32 +- 24 files changed, 1388 insertions(+), 45 deletions(-) create mode 100644 modules/miscutil/lib/upgrades/invenio_2014_10_09_author_autocompletion.py create mode 100644 modules/webstyle/css/author_autocompletion.css create mode 100644 modules/websubmit/lib/author_sources/Makefile.am create mode 100644 modules/websubmit/lib/author_sources/__init__.py create mode 100644 modules/websubmit/lib/author_sources/bibauthority.py create mode 100644 modules/websubmit/lib/functions/process_authors_json.py create mode 100644 modules/websubmit/lib/websubmit_unit_tests.py diff --git a/Makefile.am b/Makefile.am index 6f44617c95..c341e8ebef 100644 --- a/Makefile.am +++ b/Makefile.am @@ -205,6 +205,10 @@ install-jquery-plugins: wget -N --no-check-certificate http://invenio-software.org/download/jquery/parsley.js &&\ wget -N --no-check-certificate http://invenio-software.org/download/jquery/spin.min.js &&\ rm -f jquery.bookmark.package-1.4.0.zip && \ + wget https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.3.0/handlebars.min.js && \ + wget https://twitter.github.com/typeahead.js/releases/0.10.5/typeahead.bundle.min.js && \ + wget https://raw.githubusercontent.com/es-shims/es5-shim/v4.0.3/es5-shim.min.js && \ + wget https://raw.githubusercontent.com/es-shims/es5-shim/v4.0.3/es5-shim.map && \ mkdir -p ${prefix}/var/www/img && \ cd ${prefix}/var/www/img && \ wget -r -np -nH --cut-dirs=4 -A "png,css" -P jquery-ui/themes http://jquery-ui.googlecode.com/svn/tags/1.8.17/themes/base/ && \ @@ -236,6 +240,10 @@ uninstall-jquery-plugins: rm -f jquery.dataTables.min.js && \ rm -f ui.core.js && \ rm -f jquery.bookmark.min.js && \ + rm -f handlebars.min.js && \ + rm -f typeahead.bundle.min.js && \ + rm -f es5-shim.min.js && \ + rm -f es5-shim.map && \ rm -f jquery.dataTables.ColVis.min.js && \ rm -f jquery.hotkeys.js && \ rm -f jquery.tablesorter.min.js && \ diff --git a/configure.ac b/configure.ac index 9cce278beb..f634d004fd 100644 --- a/configure.ac +++ b/configure.ac @@ -923,6 +923,7 @@ AC_CONFIG_FILES([config.nice \ modules/websubmit/etc/Makefile \ modules/websubmit/lib/Makefile \ modules/websubmit/lib/functions/Makefile \ + modules/websubmit/lib/author_sources/Makefile \ modules/websubmit/web/Makefile \ modules/websubmit/web/admin/Makefile \ modules/docextract/Makefile \ diff --git a/modules/bibfield/lib/bibfield_utils.py b/modules/bibfield/lib/bibfield_utils.py index 6d29a0a9fa..eb508d531f 100644 --- a/modules/bibfield/lib/bibfield_utils.py +++ b/modules/bibfield/lib/bibfield_utils.py @@ -443,3 +443,21 @@ def _validate(self, document, schema=None, update=False): self._validate_required_fields() return len(self._errors) == 0 + +def retrieve_authorid_type(id_string): + """Retrieve the type part of the author id_string (e.g. inspireid).""" + + if not id_string or type(id_string) is not str: + return "" + if id_string.find("|(") != -1 and id_string.split("|(")[1].find(")") != -1: + return id_string.split("|(")[1].split(")")[0] + return "id" + +def retrieve_authorid_id(id_string): + """Retrieve the id part of the author id_string.""" + + if not id_string or type(id_string) is not str: + return "" + if id_string.find("|(") != -1 and id_string.split("|(")[1].find(")") != -1: + return id_string.split(")")[1] + return "" diff --git a/modules/miscutil/demo/democfgdata.sql b/modules/miscutil/demo/democfgdata.sql index 490036c60d..f8904aae57 100755 --- a/modules/miscutil/demo/democfgdata.sql +++ b/modules/miscutil/demo/democfgdata.sql @@ -1211,7 +1211,7 @@ INSERT INTO sbmFIELD VALUES ('SRVDEMOPIC',1,2,'DEMOPIC_CONT','

< INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,1,'DEMOTHE_REP','

Submit an ATLANTIS Thesis:

Your thesis will be given a reference number automatically.
However, if it has other reference numbers, please enter them here:
(one per line)
','O','Other Report Numbers','','2008-03-02','2008-03-06',NULL,NULL); INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,2,'DEMOTHE_TITLE','

*Thesis Title:
','M','Title','','2008-03-02','2008-03-06',NULL,NULL); INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,3,'DEMOTHE_SUBTTL','

Thesis Subtitle (if any):
','O','Subtitle','','2008-03-02','2008-03-06',NULL,NULL); -INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,4,'DEMOTHE_AU','

*Author of the Thesis: (one per line)
','M','Author(s)','','2008-03-02','2008-03-06',NULL,NULL); +INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,4,'DEMOTHE_AU','

*Author of the Thesis:
','M','Author(s)','','2008-03-02','2008-03-06',NULL,NULL); INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,5,'DEMOTHE_SUPERV','

Thesis Supervisor(s): (one per line)
','O','Thesis Supervisor(s)','','2008-03-02','2008-03-06',NULL,NULL); INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,6,'DEMOTHE_ABS','

*Abstract:
','M','Abstract','','2008-03-02','2008-03-06',NULL,NULL); INSERT INTO sbmFIELD VALUES ('SBIDEMOTHE',1,7,'DEMOTHE_NUMP','

Number of Pages: ','O','Number of Pages','','2008-03-02','2008-03-06',NULL,NULL); @@ -1302,7 +1302,7 @@ INSERT INTO sbmFIELDDESC VALUES ('DEMOPIC_CONT',NULL,'','D',NULL,NULL,NULL,NULL, INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_REP',NULL,'088__a','T',NULL,4,30,NULL,NULL,NULL,'2008-03-02','2008-03-02','
Other Report Numbers (one per line):',NULL,0); INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_TITLE',NULL,'245__a','T',NULL,5,60,NULL,NULL,NULL,'2008-03-02','2008-03-02','
Title:
',NULL,0); INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_SUBTTL',NULL,'245__b','T',NULL,3,60,NULL,NULL,NULL,'2008-03-02','2008-03-02','

Thesis Subtitle (if any):
',NULL,0); -INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_AU',NULL,'100__a','T',NULL,6,60,NULL,NULL,NULL,'2008-03-02','2008-03-02','
Authors:
(one per line):
',NULL,0); +INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_AU',NULL,'100__a','R',NULL,6,60,NULL,NULL,'from invenio.websubmit_engine import get_authors_autocompletion\r\n\r\nrecid = action == "MBI" and sysno or None\r\nauthor_sources = ["bibauthority"]\r\nextra_options = {\r\n "allow_custom_authors": True,\r\n "highlight_principal_author": True,\r\n}\r\nextra_fields = {\r\n "contribution": False,\r\n}\r\n\r\ntext = get_authors_autocompletion(\r\n element=element,\r\n recid=recid,\r\n curdir=curdir,\r\n author_sources=author_sources,\r\n extra_options=extra_options,\r\n extra_fields=extra_fields\r\n)','2008-03-02','2014-06-30','
Authors:
(one per line):
',NULL,0); INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_SUPERV',NULL,'','T',NULL,6,60,NULL,NULL,NULL,'2008-03-02','2008-03-02','
Thesis Supervisor(s)
(one per line):
',NULL,0); INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_ABS',NULL,'520__a','T',NULL,12,80,NULL,NULL,NULL,'2008-03-02','2008-03-02','
Abstract:
',NULL,0); INSERT INTO sbmFIELDDESC VALUES ('DEMOTHE_NUMP',NULL,'300__a','I',5,NULL,NULL,NULL,NULL,NULL,'2008-03-02','2008-03-06','
Number of Pages: ',NULL,0); @@ -1414,11 +1414,12 @@ INSERT INTO sbmFUNCTIONS VALUES ('SRV','DEMOPIC','Mail_Submitter',40,2); INSERT INTO sbmFUNCTIONS VALUES ('SRV','DEMOPIC','Move_Uploaded_Files_to_Storage',30,2); INSERT INTO sbmFUNCTIONS VALUES ('SRV','DEMOPIC','Is_Original_Submitter',20,2); INSERT INTO sbmFUNCTIONS VALUES ('SRV','DEMOPIC','Get_Recid',10,2); -INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Move_to_Done',90,1); -INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Mail_Submitter',80,1); -INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Make_Record',50,1); -INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Insert_Record',60,1); -INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Print_Success',70,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Move_to_Done',100,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Mail_Submitter',90,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Print_Success',80,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Insert_Record',70,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Make_Record',60,1); +INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','process_authors_json',50,1); INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Move_Files_to_Storage',40,1); INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Stamp_Uploaded_Files',30,1); INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','Report_Number_Generation',20,1); @@ -1427,11 +1428,12 @@ INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Get_Report_Number',10,1); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Get_Recid',20,1); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Is_Original_Submitter',30,1); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Create_Modify_Interface',40,1); -INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Move_to_Done',80,2); -INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Send_Modify_Mail',70,2); -INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Print_Success_MBI',60,2); -INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Insert_Modify_Record',50,2); -INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Make_Modify_Record',40,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Move_to_Done',90,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Send_Modify_Mail',80,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Print_Success_MBI',70,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Insert_Modify_Record',60,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Make_Modify_Record',50,2); +INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','process_authors_json',40,2); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Is_Original_Submitter',30,2); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Get_Recid',20,2); INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','Get_Report_Number',10,2); @@ -1621,6 +1623,7 @@ INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','fieldnameMBI','DEMOTHE_CHANGE'); INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','modifyTemplate','DEMOTHEmodify.tpl'); INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','addressesMBI',''); INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','sourceDoc','Thesis'); +INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','authors_json','DEMOTHE_AU'); INSERT INTO sbmPARAMETERS VALUES ('DEMOART','addressesMBI',''); INSERT INTO sbmPARAMETERS VALUES ('DEMOART','authorfile','DEMOART_AU'); INSERT INTO sbmPARAMETERS VALUES ('DEMOART','autorngen','Y'); diff --git a/modules/miscutil/lib/upgrades/invenio_2014_10_09_author_autocompletion.py b/modules/miscutil/lib/upgrades/invenio_2014_10_09_author_autocompletion.py new file mode 100644 index 0000000000..57dbd8bc13 --- /dev/null +++ b/modules/miscutil/lib/upgrades/invenio_2014_10_09_author_autocompletion.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014 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. + +"""Upgrade recipe for new column tag.recjson_value.""" + +import os + +from invenio.dbquery import run_sql +from invenio.config import CFG_PREFIX + +depends_on = ['invenio_release_1_1_0'] + + +def info(): + """Upgrade recipe information.""" + return "Set up autocompletion for DEMOTHE authors" + + +def do_upgrade(): + """Upgrade recipe procedure.""" + os.system("cd %(prefix)s/var/www/js && \ + wget https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.3.0/handlebars.min.js && \ + wget https://twitter.github.com/typeahead.js/releases/0.10.5/typeahead.bundle.min.js && \ + wget https://raw.githubusercontent.com/es-shims/es5-shim/v4.0.3/es5-shim.min.js && \ + wget https://raw.githubusercontent.com/es-shims/es5-shim/v4.0.3/es5-shim.map" + % {'prefix': CFG_PREFIX}) + + # Remove "one line per author" info on author textbox + run_sql("""UPDATE sbmFIELD + set fitext='

' @@ -1279,7 +1308,13 @@ def tmpl_add_comment_form(self, recID, uid, nickname, ln, msg, 'x_nickname': '
' + display + ''} if not CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR: - note += '
' + ' '*10 + cgi.escape('You can use some HTML tags: , ,
,
,

, ,

*Author of the Thesis:
' + where fidesc="DEMOTHE_AU";""") + + # Add the response logic to the DEMOTHE_AU element + run_sql("""REPLACE sbmFIELDDESC VALUES ('DEMOTHE_AU',NULL,'100__a','R',NULL,6,60,NULL,NULL,'from invenio.websubmit_engine import get_authors_autocompletion\r\n\r\nrecid = action == "MBI" and sysno or None\r\nauthor_sources = ["bibauthority"]\r\nextra_options = {\r\n "allow_custom_authors": True,\r\n "highlight_principal_author": True,\r\n}\r\nextra_fields = {\r\n "contribution": False,\r\n}\r\n\r\ntext = get_authors_autocompletion(\r\n element=element,\r\n recid=recid,\r\n curdir=curdir,\r\n author_sources=author_sources,\r\n extra_options=extra_options,\r\n extra_fields=extra_fields\r\n)','2008-03-02','2014-06-30','',NULL,0);""") + + # Create the process_author_json_function + run_sql("INSERT INTO sbmFUNDESC VALUES ('process_authors_json','authors_json');") + + # Add it to the DEMOTHE workflow + run_sql("INSERT INTO sbmPARAMETERS VALUES ('DEMOTHE','authors_json','DEMOTHE_AU');") + + # Add proccess_author_json into the submission function sequence for DEMOTHESIS + run_sql("INSERT INTO sbmFUNCTIONS VALUES ('SBI','DEMOTHE','process_authors_json',50,1);") + run_sql("UPDATE sbmFUNCTIONS set score=100 where action='SBI' and doctype='DEMOTHE' and function='Move_to_Done';") + run_sql("UPDATE sbmFUNCTIONS set score=90 where action='SBI' and doctype='DEMOTHE' and function='Mail_Submitter';") + run_sql("UPDATE sbmFUNCTIONS set score=80 where action='SBI' and doctype='DEMOTHE' and function='Print_Success';") + run_sql("UPDATE sbmFUNCTIONS set score=70 where action='SBI' and doctype='DEMOTHE' and function='Insert_Record';") + run_sql("UPDATE sbmFUNCTIONS set score=60 where action='SBI' and doctype='DEMOTHE' and function='Make_Record';") + + # Add proccess_author_json into the modification function sequence for DEMOTHESIS + run_sql("INSERT INTO sbmFUNCTIONS VALUES ('MBI','DEMOTHE','process_authors_json',40,2);") + run_sql("UPDATE sbmFUNCTIONS set score=90 where action='MBI' and doctype='DEMOTHE' and function='Move_to_Done';") + run_sql("UPDATE sbmFUNCTIONS set score=80 where action='MBI' and doctype='DEMOTHE' and function='Send_Modify_Mail';") + run_sql("UPDATE sbmFUNCTIONS set score=70 where action='MBI' and doctype='DEMOTHE' and function='Print_Success_MBI';") + run_sql("UPDATE sbmFUNCTIONS set score=60 where action='MBI' and doctype='DEMOTHE' and function='Insert_Modify_Record';") + run_sql("UPDATE sbmFUNCTIONS set score=50 where action='MBI' and doctype='DEMOTHE' and function='Make_Modify_Record';") + + +def estimate(): + """Upgrade recipe time estimate.""" + return 1 diff --git a/modules/miscutil/sql/tabfill.sql b/modules/miscutil/sql/tabfill.sql index 9ed0746167..50027098d2 100644 --- a/modules/miscutil/sql/tabfill.sql +++ b/modules/miscutil/sql/tabfill.sql @@ -857,6 +857,7 @@ INSERT INTO sbmFUNDESC VALUES ('Run_PlotExtractor','with_docname'); INSERT INTO sbmFUNDESC VALUES ('Run_PlotExtractor','with_doctype'); INSERT INTO sbmFUNDESC VALUES ('Run_PlotExtractor','with_docformat'); INSERT INTO sbmFUNDESC VALUES ('Run_PlotExtractor','extract_plots_switch_file'); +INSERT INTO sbmFUNDESC VALUES ('process_authors_json','authors_json'); INSERT INTO sbmGFILERESULT VALUES ('HTML','HTML document'); INSERT INTO sbmGFILERESULT VALUES ('WORD','data'); diff --git a/modules/webstyle/css/Makefile.am b/modules/webstyle/css/Makefile.am index 2cad805789..39aeafc3f5 100644 --- a/modules/webstyle/css/Makefile.am +++ b/modules/webstyle/css/Makefile.am @@ -17,7 +17,11 @@ webdir = $(localstatedir)/www/img -web_DATA = invenio.css invenio-ie7.css tablesorter.css jquery.ajaxPager.css +web_DATA = invenio.css \ + invenio-ie7.css \ + tablesorter.css \ + jquery.ajaxPager.css \ + author_autocompletion.css EXTRA_DIST = $(web_DATA) diff --git a/modules/webstyle/css/author_autocompletion.css b/modules/webstyle/css/author_autocompletion.css new file mode 100644 index 0000000000..5ef0fab0e1 --- /dev/null +++ b/modules/webstyle/css/author_autocompletion.css @@ -0,0 +1,204 @@ +/* +* -*- mode: text; coding: utf-8; -*- + + This file is part of Invenio. + Copyright (C) 2014 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. +*/ + +.typeahead, +.tt-query, +.tt-hint { + width: 400px; + height: 20px; + padding: 8px 14px; + font-size: 13px; + border: 2px solid #ccc; + outline: none; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.typeahead { + background-color: #fff; +} +.author-row{ + background: #fff; + margin-bottom: 5px; + padding: 10px; + border: 1px solid #ccc; + border-left: 2px solid #ccc; + position: relative; + -webkit-transition: border 1s ease-in-out; + -moz-transition: border 1s ease-in-out; + -o-transition: border 1s ease-in-out; + transition: border 1s ease-in-out; +} +.author-principal{ + border-left: 2px solid #4B77BE; +} +.author-row-header-principal{ + line-height: 20px!important; + padding: 0px; +} +.author-row-header-principal .label { + display: inline-block; + font-size: 11px; + line-height: normal; + color: #fff; + padding: 1px; + background: #4B77BE; + border: 1px solid #3A539B; +} +.author-row:hover { + cursor: move; +} +.author-row:hover .author-row-header-delete{ + visibility: visible; + cursor: default!important; +} +.author-row-header-delete{ + position: absolute; + right: 5px; + height: 90%; + width: 30%; + text-align: right; + top: 5px; + visibility: hidden; +} +.author-row-header-name { + font-size: 18px; + padding-bottom: 2px; + color: #111; +} +.author-row-body-extra { + color: #444; +} +.author-row-body-affiliation { + color: #444; +} +.author-row-body-email { + padding-bottom: 10px; + font-size: 12px; +} + +.author-row-body { + color: #666; + font-size: 14px; +} +.author-empty-message{ + padding: 10px 20px; +} +.author-empty-message h2{ + margin: 0; + padding: 0 0 5px; + line-height: normal; + color: #111; + font-size: 16px; +} +.author-empty-message p{ + color: #757575; + margin: 0; + padding: 0; + line-height: normal; + font-size: 11px; +} +.autocomplete-author-row{ + +} +.autocomplete-author-row-name{ + font-size: 16px; + line-height: normal; + padding: 0; +} +.autocomplete-author-row-email{ + font-size: 10px; + line-height: normal; + padding: 0 0 5px; + color: #666; +} +.autocomplete-author-row-affiliation{ + font-size: 11px; + line-height: normal; + padding: 0; + color: #666; +} +.typeahead:focus { + border: 2px solid #0097cf; +} +.tt-hint { + color: #999 +} +.tt-dropdown-menu { + min-height: 50px; + max-height: 300px; + overflow-y: scroll; + width: 650px; + margin-top: 0px; + padding: 0px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2) +} + +.tt-suggestion { + padding: 5px 20px; + font-size: 18px; + line-height: 24px; + border-bottom:1px solid #ddd; + +} + +.tt-suggestion.tt-cursor { + color: #fff; + cursor: pointer; + background-color:#0097cf; +} + +.tt-suggestion.tt-cursor .autocomplete-author-row-email, +.tt-suggestion.tt-cursor .autocomplete-author-row-affiliation{ + color: #fff!important; +} + +.tt-suggestion p { + margin: 0; +} + +/*websubmit author autocompletion */ +#websubmit_authors_table { + + padding: 0 0px; + max-width: 100%; + width: 664px; + margin: 20px 0; +} +#websubmit_authors_tbody { + width: 664px; +} +.websubmit_authors_tr { + border-collapse: collapse; + vertical-align: middle; +} +.websubmit_authors_list { + display: block; + margin-top: 10px; + padding: 5px; + border: 1px solid #ccc; + background: #fff; + overflow: hidden; + margin-bottom:10px; +} diff --git a/modules/websubmit/etc/DEMOTHE.tpl b/modules/websubmit/etc/DEMOTHE.tpl index 143ba8f728..03d4647222 100644 --- a/modules/websubmit/etc/DEMOTHE.tpl +++ b/modules/websubmit/etc/DEMOTHE.tpl @@ -16,3 +16,8 @@ DEMOTHE_PLACE---<:DEMOTHE_PLACE:> DEMOTHE_UNIV---<:DEMOTHE_UNIV:> DEMOTHE_ABS---<:DEMOTHE_ABS:> DEMOTHE_FILE_RENAMED---<:DEMOTHE_FILE_RENAMED:> +AUTHOR_FULLNAME---<:AUTHOR_FULLNAME:> +AUTHOR_ID---<:AUTHOR_ID:> +AUTHOR_EMAIL---<:AUTHOR_EMAIL:> +AUTHOR_AFFILIATION---<:AUTHOR_AFFILIATION:> +AUTHOR_CONTRIBUTION---<:AUTHOR_CONTRIBUTION:> diff --git a/modules/websubmit/etc/DEMOTHEcreate.tpl b/modules/websubmit/etc/DEMOTHEcreate.tpl index 9fab9dd4ad..3574484d6b 100644 --- a/modules/websubmit/etc/DEMOTHEcreate.tpl +++ b/modules/websubmit/etc/DEMOTHEcreate.tpl @@ -3,14 +3,14 @@ START::DEFP()--- 037a::REPL(EOL,)::MINLW(82)---<:DEMOTHE_RN::DEMOTHE_RN:> 041a::REPL(EOL,)::MINLW(82)---<:DEMOTHE_LANG::DEMOTHE_LANG::IF(Select:,eng,ORIG):> 088a::REP(EOL,)::MINLW(82)---<:DEMOTHE_REP*::DEMOTHE_REP:> -100a::REP(EOL,)::RANGE(1,1)::MINLW(82)---<:DEMOTHE_AU*::DEMOTHE_AU:> +100a::REP(EOL,)::RANGE(1,1)::MINLW(82)---<:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,ORIG):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,ORIG):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,ORIG):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,ORIG):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,ORIG):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):> 245a::REPL(EOL,)::MINLW(82)---<:DEMOTHE_TITLE::DEMOTHE_TITLE:><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL::IF(,,):><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL:><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL::IF(,,):> 260::REPL(EOL,)::MINLW(82)---<:DEMOTHE_PUBL::DEMOTHE_PUBL::IF(,,):><:DEMOTHE_PUBL::DEMOTHE_PUBL:><:DEMOTHE_PUBL::DEMOTHE_PUBL::IF(,,):><:DEMOTHE_PLDEF::DEMOTHE_PLDEF::IF(,,):><:DEMOTHE_PLDEF::DEMOTHE_PLDEF:><:DEMOTHE_PLDEF::DEMOTHE_PLDEF::IF(,,):><:DEMOTHE_DATE::year:> 300::REPL(EOL,)::MINLW(82)---<:DEMOTHE_NUMP::DEMOTHE_NUMP::IF(,mult. p,ORIG):> 500a::REPL(EOL,)::MINLW(97)---Presented on <:DEMOTHE_DATE::dd:> <:DEMOTHE_DATE::mm:> <:DEMOTHE_DATE::year:> 502::REPL(EOL,)::MINLW(144)---<:DEMOTHE_DIPL::DEMOTHE_DIPL:><:DEMOTHE_PLACE::DEMOTHE_PLACE:>, <:DEMOTHE_UNIV::DEMOTHE_UNIV:><:DEMOTHE_DATE::year:> 520a::REP(EOL,)::MINLW(82)---<:DEMOTHE_ABS::DEMOTHE_ABS:> -700a::REP(EOL,)::RANGE(2,1999)::MINLW(82)---<:DEMOTHE_AU*::DEMOTHE_AU:> +700a::REP(EOL,)::RANGE(2,1999)::MINLW(82)---<:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,ORIG):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,ORIG):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,ORIG):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,ORIG):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,ORIG):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):> 700a::REP(EOL,)::MINLW(116)---<:DEMOTHE_SUPERV*::DEMOTHE_SUPERV:>dir. 856f::REPL(EOL,)---<:SuE::SuE:> 980::DEFP()---THESIS diff --git a/modules/websubmit/etc/DEMOTHEmodify.tpl b/modules/websubmit/etc/DEMOTHEmodify.tpl index cb28eb2186..27b7d3bfaf 100644 --- a/modules/websubmit/etc/DEMOTHEmodify.tpl +++ b/modules/websubmit/etc/DEMOTHEmodify.tpl @@ -2,10 +2,10 @@ START::DEFP()--- 001::REPL(EOL,)---<:SN::SN:> 041a::REPL(EOL,)::MINLW(82)---<:DEMOTHE_LANG::DEMOTHE_LANG::IF(Select:,,ORIG):> 088::REPL(EOL,)::MINLW(82)---<:DEMOTHE_REP*::DEMOTHE_REP:> -100::REPL(EOL,)::RANGE(1,1)::MINLW(82)---<:DEMOTHE_AU*::DEMOTHE_AU:> +100::REP(EOL,)::RANGE(1,1)::MINLW(82)---<:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,ORIG):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,ORIG):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,ORIG):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,ORIG):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,ORIG):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):> 245a::REPL(EOL,)::MINLW(82)---<:DEMOTHE_TITLE::DEMOTHE_TITLE:><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL::IF(,,):><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL:><:DEMOTHE_SUBTTL::DEMOTHE_SUBTTL::IF(,,):> 300::REPL(EOL,)::MINLW(82)---<:DEMOTHE_NUMP::DEMOTHE_NUMP:> 520a::REP(EOL,)::MINLW(82)---<:DEMOTHE_ABS::DEMOTHE_ABS:> -700a::REP(EOL,)::RANGE(2,1999)::MINLW(82)---<:DEMOTHE_AU*::DEMOTHE_AU:> +700a::REP(EOL,)::RANGE(2,1999)::MINLW(82)---<:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,ORIG):><:AUTHOR_FULLNAME*::AUTHOR_FULLNAME::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,ORIG):><:AUTHOR_ID*::AUTHOR_ID::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,ORIG):><:AUTHOR_CONTRIBUTION*::AUTHOR_CONTRIBUTION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,ORIG):><:AUTHOR_AFFILIATION*::AUTHOR_AFFILIATION::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,ORIG):><:AUTHOR_EMAIL*::AUTHOR_EMAIL::IF(#None#, ,):> 700a::REP(EOL,)::MINLW(116)---<:DEMOTHE_SUPERV*::DEMOTHE_SUPERV:>dir. END::DEFP()--- diff --git a/modules/websubmit/lib/Makefile.am b/modules/websubmit/lib/Makefile.am index 9f36a294ec..70d8a1e95f 100644 --- a/modules/websubmit/lib/Makefile.am +++ b/modules/websubmit/lib/Makefile.am @@ -15,15 +15,18 @@ # along with Invenio; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -SUBDIRS = functions +SUBDIRS = author_sources \ + functions pylibdir = $(libdir)/python/invenio -pylib_DATA = websubmit_config.py websubmit_engine.py \ +pylib_DATA = websubmit_config.py \ + websubmit_engine.py \ websubmit_dblayer.py \ websubmit_webinterface.py \ websubmit_templates.py \ websubmit_regression_tests.py \ + websubmit_unit_tests.py \ websubmitadmin_config.py \ websubmitadmin_dblayer.py \ websubmitadmin_engine.py \ diff --git a/modules/websubmit/lib/author_sources/Makefile.am b/modules/websubmit/lib/author_sources/Makefile.am new file mode 100644 index 0000000000..f98a8c9dc4 --- /dev/null +++ b/modules/websubmit/lib/author_sources/Makefile.am @@ -0,0 +1,25 @@ +# This file is part of Invenio. +# Copyright (C) 2014 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/websubmit_author_sources + +pylib_DATA = __init__.py \ + bibauthority.py + +EXTRA_DIST = $(pylib_DATA) + +CLEANFILES = *~ *.tmp *.pyc diff --git a/modules/websubmit/lib/author_sources/__init__.py b/modules/websubmit/lib/author_sources/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/websubmit/lib/author_sources/bibauthority.py b/modules/websubmit/lib/author_sources/bibauthority.py new file mode 100644 index 0000000000..9ea3d18640 --- /dev/null +++ b/modules/websubmit/lib/author_sources/bibauthority.py @@ -0,0 +1,54 @@ +# This file is part of Invenio. +# Copyright (C) 2014 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.search_engine import perform_request_search +from invenio.bibfield import get_record +from invenio.bibfield_utils import retrieve_authorid_type,retrieve_authorid_id +from invenio.websubmit_config import CFG_SUBFIELFD_TO_JSON_FIELDS + +CFG_SOURCE_NAME = "bibauthority" +CFG_LIMIT_RESULTS = 100 + +def query_author_source(nickname): + """ + Query the current database for records that belong to + the collection "People" and have the string nickname + inside them, so the can be used for autocompletion. + """ + + # query for matches + recids = perform_request_search(c="People", p=nickname) + authors = [] + + # Convert the database results into a dictionary with the + # id fields values cleanly separated from their id type + # for the frontend javascript code to reconstruct the + # existing authors list as it was when it was submitted + for recid in recids[:CFG_LIMIT_RESULTS]: + + record = get_record(recid) + + author = { + "name": record["authors"][0]["full_name"], + } + author.update({ + CFG_SUBFIELFD_TO_JSON_FIELDS["0"].get(retrieve_authorid_type(x["value"])) or retrieve_authorid_type(x["value"]): retrieve_authorid_id(x["value"]) for x in record["system_control_number"] if type(x) is dict + }) + + authors.append(author) + + return authors diff --git a/modules/websubmit/lib/functions/Create_Modify_Interface.py b/modules/websubmit/lib/functions/Create_Modify_Interface.py index acdc74531d..e036f8f16d 100644 --- a/modules/websubmit/lib/functions/Create_Modify_Interface.py +++ b/modules/websubmit/lib/functions/Create_Modify_Interface.py @@ -14,10 +14,12 @@ # 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. + """ This is the Create_Modify_Interface function (along with its helpers). It is used by WebSubmit for the "Modify Bibliographic Information" action. """ + __revision__ = "$Id$" import os @@ -305,9 +307,10 @@ def Create_Modify_Interface(parameters, curdir, form, user_info=None): ## Note this exec is safe WRT global variable because the ## Create_Modify_Interface has already been parsed by ## execfile within a protected environment. - the_globals['text'] = '' + the_globals["text"] = "" + the_globals["element"] = {"name": field, "value": value} exec co in the_globals - text = the_globals['text'] + text = the_globals["text"] except: msg = "Error in evaluating response element %s with globals %s" % (pprint.pformat(field), pprint.pformat(globals())) register_exception(req=None, alert_admin=True, prefix=msg) diff --git a/modules/websubmit/lib/functions/Makefile.am b/modules/websubmit/lib/functions/Makefile.am index cd2877551b..c12f4dfaf2 100644 --- a/modules/websubmit/lib/functions/Makefile.am +++ b/modules/websubmit/lib/functions/Makefile.am @@ -60,6 +60,7 @@ pylib_DATA = __init__.py \ Print_Success_DEL.py \ Print_Success_MBI.py \ Print_Success_SRV.py \ + process_authors_json.py \ Register_Approval_Request.py \ Register_Referee_Decision.py \ Withdraw_Approval_Request.py \ diff --git a/modules/websubmit/lib/functions/process_authors_json.py b/modules/websubmit/lib/functions/process_authors_json.py new file mode 100644 index 0000000000..da30f84067 --- /dev/null +++ b/modules/websubmit/lib/functions/process_authors_json.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014, 2015 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. + +__revision__ = "$Id$" + +import os +import json +from invenio.websubmit_functions.Shared_Functions import \ + get_dictionary_from_string, \ + ParamFromFile, \ + write_file +from invenio.websubmit_config import \ + CFG_SUBFIELD_DEFINITIONS, \ + CFG_JSON_TO_TPL_FIELDS, \ + CFG_AUTHORITY_CONTAINER_DICTIONARY, \ + CFG_TPL_FIELDS + +def encapsulate_id(id_dict, key, value): + """ + """ + + if (key in id_dict) and str(value).strip(): + return id_dict[key] % value + else: + return value + +def process_authors_json(parameters, curdir, form, user_info=None): + """ + Converts the field value (from its repective file) from JSON + to a format that bibconvert understands. + """ + + global sysno + + ## the name of the field that has the json inside + json_field = parameters.get("authors_json", None) + + ## separators in case a field has more than one values + field_key_to_separator = { + "AUTHOR_ID": CFG_SUBFIELD_DEFINITIONS["id"], + } + + filename = "%s/%s" % (curdir, json_field) + if os.path.exists(filename): + + ## open the file that corresponds to the field + ## and read its contents into a dictionary + json_str = ParamFromFile(os.path.join(curdir, filename)) + obj = json.loads(json_str) + + field_values = [] + # For all the items in the field iterate their key,value + # and place them in an array of dictionaries which contain + # the tpl names that are going to be used as the keys and + # their values as values. + # (e.g. field_values[0]["AUTHOR_FULLNAME"] = "John Ellis") + for items in obj["items"]: + field_values.append(dict(CFG_TPL_FIELDS)) + # Make sure the the `field_key_to_separator` keys are list + for key in field_key_to_separator.keys(): + field_values[-1][key] = [] + + for key, value in items.iteritems(): + if key in CFG_JSON_TO_TPL_FIELDS.keys(): + slug_id = CFG_JSON_TO_TPL_FIELDS[key] + encapsulated = encapsulate_id( + CFG_AUTHORITY_CONTAINER_DICTIONARY, key, value + ) + if slug_id in field_key_to_separator.keys(): + # Make sure that the appeded items are not empty + if value: + field_values[-1][slug_id].append(encapsulated) + else: + field_values[-1][slug_id] = encapsulated + + fields = list(set(CFG_JSON_TO_TPL_FIELDS.itervalues())) + # For every field, take the field_values of each of the + # elements and join the values (even the empty ones), + # separating them with a newline, so bibconvert knows + # which value corresponds to each element(author). + for field in fields: + attributes_to_write = [] + for field_value in field_values: + if field in field_key_to_separator.keys(): + items = field_value.get(field, "") + items_size = len(items) + if items_size > 1: + items[0] = "{0}".format(items[0]) + items[-1] = "{1}".format( + field_key_to_separator.get(field), items[-1] + ) + if items_size > 2: + for i in range(1, items_size - 1): + items[i] = "{1}".format( + field_key_to_separator.get(field), items[i] + ) + val = "".join(items) + else: + val = str(field_value.get(field,"")).strip() + # Check if value is empty + val = val or "#None#" + attributes_to_write.append(val) + attributes_to_write = "\n".join(attributes_to_write) + write_file(os.path.join(curdir,field),attributes_to_write) diff --git a/modules/websubmit/lib/websubmit_config.py b/modules/websubmit/lib/websubmit_config.py index 104af239ee..61d415d985 100644 --- a/modules/websubmit/lib/websubmit_config.py +++ b/modules/websubmit/lib/websubmit_config.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # This file is part of Invenio. # Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. # @@ -19,9 +21,6 @@ __revision__ = "$Id$" -import re - -# test: test = "FALSE" # CC all action confirmation mails to administrator? (0 == NO; 1 == YES) @@ -54,6 +53,57 @@ # Prefix for video uploads, Garbage Collector CFG_WEBSUBMIT_TMP_VIDEO_PREFIX = "video_upload_" +# Mapping of each name to its subfield +CFG_SUBFIELD_DEFINITIONS = { + "name": "a", + "id": "0", + "contribution": "g", + "affiliation": "u", + "email": "m" +} + +# Mapping of external name of fields to internal names fields +CFG_SUBFIELFD_TO_JSON_FIELDS = { + CFG_SUBFIELD_DEFINITIONS["name"]: "name", + CFG_SUBFIELD_DEFINITIONS["id"]: { + "id": "id", + "SzGeCERN" : "cernccid", + "INSPIRE": "inspireid" + }, + CFG_SUBFIELD_DEFINITIONS["contribution"]: "contribution", + CFG_SUBFIELD_DEFINITIONS["affiliation"]: "affiliation", + CFG_SUBFIELD_DEFINITIONS["email"]: "email" +} + +# Mapping of the internal field names to tpl attribute and file names +CFG_JSON_TO_TPL_FIELDS = { + "id": "AUTHOR_ID", + "inspireid": "AUTHOR_ID", + "name": "AUTHOR_FULLNAME", + "firstname": "AUTHOR_FULLNAME", + "lastname": "AUTHOR_FULLNAME", + "cernccid": "AUTHOR_ID", + "affiliation": "AUTHOR_AFFILIATION", + "email": "AUTHOR_EMAIL", + "contribution": "AUTHOR_CONTRIBUTION" +} + +# Mapping from the id type name to id_encaptulation string used to +# format ids depending on their type. +CFG_AUTHORITY_CONTAINER_DICTIONARY = { + "id": "AUTHOR|(ID)%s", + "inspireid": "AUTHOR|(INSPIRE)%s", + "cernccid": "AUTHOR|(SzGeCERN)%s" +} + +# Create a template dictionary that has all the values from +# CFG_AUTHORITY_CONTAINER_DICTIONARY as its keys and has no values +# It will be used to create the dictionary for each element to be used +# when writing the files for bibconvert. Even the empty elements will +# be written with a specific notation so bibconvert does not mix up the +# values of the different elements. +CFG_TPL_FIELDS = reduce(lambda x, y: x.update(y) or x, [{f: ""} for f in set(CFG_JSON_TO_TPL_FIELDS.itervalues())]) + class InvenioWebSubmitFunctionError(Exception): """This exception should only ever be raised by WebSubmit functions. It will be caught and handled by the WebSubmit core itself. diff --git a/modules/websubmit/lib/websubmit_engine.py b/modules/websubmit/lib/websubmit_engine.py index 0a17ec9349..05bc89e593 100644 --- a/modules/websubmit/lib/websubmit_engine.py +++ b/modules/websubmit/lib/websubmit_engine.py @@ -21,7 +21,6 @@ __revision__ = "$Id$" -# import interesting modules: import traceback import string import os @@ -30,8 +29,9 @@ import types import re import pprint -from urllib import quote_plus +from urllib import quote_plus, unquote_plus from cgi import escape +from json import dumps from invenio.config import \ CFG_SITE_LANG, \ @@ -42,22 +42,28 @@ CFG_DEVEL_SITE, \ CFG_SITE_SECURE_URL, \ CFG_WEBSUBMIT_USE_MATHJAX - from invenio.dbquery import Error from invenio.access_control_engine import acc_authorize_action from invenio.webpage import page, error_page, warning_page from invenio.webuser import getUid, get_email, collect_user_info, isGuestUser, \ page_not_authorized -from invenio.websubmit_config import CFG_RESERVED_SUBMISSION_FILENAMES, \ - InvenioWebSubmitFunctionError, InvenioWebSubmitFunctionStop, \ - InvenioWebSubmitFunctionWarning from invenio.messages import gettext_set_language, wash_language from invenio.webstat import register_customevent from invenio.errorlib import register_exception from invenio.urlutils import make_canonical_urlargd, redirect_to_url -from invenio.websubmitadmin_engine import string_is_alphanumeric_including_underscore from invenio.htmlutils import get_mathjax_header +from invenio.bibfield_utils import retrieve_authorid_type, \ + retrieve_authorid_id +from invenio.pluginutils import PluginContainer +from invenio.search_engine import get_record +from invenio.websubmit_config import CFG_RESERVED_SUBMISSION_FILENAMES, \ + CFG_SUBFIELFD_TO_JSON_FIELDS, \ + CFG_SUBFIELD_DEFINITIONS, \ + InvenioWebSubmitFunctionError, \ + InvenioWebSubmitFunctionStop, \ + InvenioWebSubmitFunctionWarning +from invenio.websubmitadmin_engine import string_is_alphanumeric_including_underscore from invenio.websubmit_dblayer import \ get_storage_directory_of_action, \ get_longname_of_doctype, \ @@ -87,6 +93,9 @@ get_functions_for_submission_step, \ get_submissions_at_level_X_with_score_above_N, \ submission_is_finished +from invenio.websubmit_functions.Shared_Functions import \ + ParamFromFile, \ + write_file import invenio.template websubmit_templates = invenio.template.load('websubmit') @@ -535,11 +544,19 @@ def interface(req, # The 'R' fields must be executed in the engine's environment, # as the runtime functions access some global and local # variables. - if full_field ['type'] == 'R': + if full_field['type'] == 'R': try: - co = compile (full_field ['htmlcode'].replace("\r\n","\n"), "", "exec") + co = compile(full_field['htmlcode'].replace( + "\r\n", "\n"), + "", + "exec" + ) the_globals['text'] = '' the_globals['custom_level'] = None + the_globals['element'] = { + "name": full_field["name"], + "value": "" + } exec co in the_globals text = the_globals['text'] # Also get the custom_level if it's define in the element description @@ -551,7 +568,10 @@ def interface(req, register_exception(req=req, alert_admin=True, prefix="Error in evaluating response element %s with globals %s" % (pprint.pformat(full_field), pprint.pformat(the_globals))) raise else: - text = websubmit_templates.tmpl_submit_field (ln = ln, field = full_field) + text = websubmit_templates.tmpl_submit_field( + ln=ln, + field=full_field + ) # Provide a default value for the custom_level custom_level = None @@ -1861,3 +1881,218 @@ def log_function(curdir, message, start_time, filename="function_log"): fd = open("%s/%s" % (curdir, filename), "a+") fd.write("""%s --- %s\n""" % (message, time_lap)) fd.close() + + +def _convert_tag_tuple_array_to_author_dictionary(record_tag): + """ + """ + + author = {} + + for _tuple in record_tag: + + if _tuple[0] == CFG_SUBFIELD_DEFINITIONS["id"]: + + if CFG_SUBFIELFD_TO_JSON_FIELDS[ + CFG_SUBFIELD_DEFINITIONS["id"] + ].get(retrieve_authorid_type(_tuple[1])): + + author[CFG_SUBFIELFD_TO_JSON_FIELDS[ + CFG_SUBFIELD_DEFINITIONS["id"] + ].get( + retrieve_authorid_type(_tuple[1]) + )] = retrieve_authorid_id(_tuple[1]).decode("string_escape") + + elif CFG_SUBFIELFD_TO_JSON_FIELDS.get(_tuple[0]): + + if (not _tuple[1] == " ") and _tuple[1]: + author[ + CFG_SUBFIELFD_TO_JSON_FIELDS.get(_tuple[0]) + ] = _tuple[1].decode("string_escape") + + return author + + +def _convert_record_authors_to_json(recid): + """ + Convert authors of a record to the original + json that was submitted with the record about + the authors. This is used to recreate the list + on the frontend when the record is requested for + modification. + """ + + record = get_record(recid) + + if record: + record_authors = record.get("100", []) + record.get("700", []) + if record_authors: + json_authors = [] + for record_author in record_authors: + json_authors.append( + _convert_tag_tuple_array_to_author_dictionary( + record_author[0] + ) + ) + return dumps({"items": json_authors}) + + else: + return "" + + else: + return "" + + +def get_authors_autocompletion( + element, + recid=None, + curdir=None, + author_sources=[], + extra_author_sources=None, + extra_options={}, + extra_fields={}, + ln=CFG_SITE_LANG +): + """ + Creates the author autocompletion form. + """ + + # Prepare the AUTHOR_SOURCES file in the submission directory. + # This file will later be checked to see if this submission has access + # to the given author sources. + write_file("%s/AUTHOR_SOURCES" % curdir, "\n".join(author_sources)) + extra_author_sources = extra_author_sources or [] + write_file( + "%s/EXTRA_AUTHOR_SOURCES" % curdir, "\n".join(extra_author_sources)) + + # In case we are modifying a record then prepare the authors to be loaded + # in the form. + if recid is not None: + element['value'] = _convert_record_authors_to_json(recid) + + # We don't want to expose the entire curdir in the query so we just use the + # relative part of it. + relative_curdir = curdir.partition(CFG_WEBSUBMIT_STORAGEDIR)[-1].strip("/") + + return websubmit_templates.tmpl_authors_autocompletion( + element, + relative_curdir=quote_plus(relative_curdir), + author_sources_p=bool(author_sources or extra_author_sources), + extra_options=extra_options, + extra_fields=extra_fields, + ln=CFG_SITE_LANG + ) + + +def get_authors_from_allowed_sources( + req, + query, + relative_curdir +): + """ + Uses pluginbuilder to import the functions from allowed plugins + to query for authors from the, correspodent to its plugin, author source. + """ + + def plugin_builder_function(plugin_name, plugin_code): + """ + """ + + return { + "source_name": getattr( + plugin_code, + "CFG_SOURCE_NAME", + None + ), + "query_function": getattr( + plugin_code, + "query_author_source", + None + ), + } + + result = [] + error = None + + relative_curdir = unquote_plus(relative_curdir) + + # Retrieve the author sources that this submission is allowed to use. + author_sources = ParamFromFile( + os.path.join( + CFG_WEBSUBMIT_STORAGEDIR, + relative_curdir, + "AUTHOR_SOURCES" + ) + ) + author_sources = filter(None, author_sources.split("\n")) + + extra_author_sources = ParamFromFile( + os.path.join( + CFG_WEBSUBMIT_STORAGEDIR, + relative_curdir, + "EXTRA_AUTHOR_SOURCES" + ) + ) + extra_author_sources = filter(None, extra_author_sources.split("\n")) + + author_sources.extend(extra_author_sources) + + if not author_sources: + error = "No author sources available" + return (result, error) + + # Retrieve the user that started this submission and match them to the user + # running this query. + SuE = ParamFromFile( + os.path.join( + CFG_WEBSUBMIT_STORAGEDIR, + relative_curdir, + "SuE" + ) + ) + user_info = collect_user_info(req) + email = user_info["email"] + if email != SuE: + error = "The current user is not authorized to perform this query" + return (result, error) + + # Load the author sources plugins + author_sources_plugins = PluginContainer( + os.path.join( + CFG_PYLIBDIR, + "invenio", + "websubmit_author_sources", + "*.py" + ), + plugin_builder_function + ) + + # For every author source check if it exists in the currently loaded + # plugins. If it does then query the source of the plugin and extend + # the array of results to include the authors that were returned. + for author_source in author_sources: + if author_source in extra_author_sources and result: + # NOTE: This means that not all extra_author_sources will + # necessarily be queried. If at least one yields results we stop + # looking into the rest of them. + break + author_source = author_source.rstrip() + if author_sources_plugins.has_key(str(author_source)): + try: + result.extend( + author_sources_plugins[author_source]["query_function"]( + query + ) + ) + except: + register_exception( + req=req, + alert_admin=True, + prefix="Error in executing plugin %s with globals %s" % ( + pprint.pformat(author_source), + pprint.pformat(traceback.format_exc()) + ) + ) + raise + + return (result, error) diff --git a/modules/websubmit/lib/websubmit_templates.py b/modules/websubmit/lib/websubmit_templates.py index 44cf5f8ca8..fec9602e68 100644 --- a/modules/websubmit/lib/websubmit_templates.py +++ b/modules/websubmit/lib/websubmit_templates.py @@ -1,5 +1,5 @@ # This file is part of Invenio. -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -21,8 +21,13 @@ import re import operator -from invenio.config import CFG_SITE_URL, CFG_SITE_LANG, CFG_SITE_RECORD, \ - CFG_SITE_SECURE_URL, CFG_INSPIRE_SITE +from invenio.config import \ + CFG_SITE_URL, \ + CFG_SITE_LANG, \ + CFG_SITE_RECORD, \ + CFG_SITE_SECURE_URL, \ + CFG_INSPIRE_SITE, \ + CFG_SITE_SUPPORT_EMAIL from invenio.messages import gettext_set_language from invenio.dateutils import convert_datetext_to_dategui from invenio.urlutils import create_html_link @@ -2898,6 +2903,400 @@ def tmpl_mathpreview_header(self, ln, https=False): 'help-label': escape_javascript_string(_("Use '\\$' delimiters to write LaTeX markup. Eg: \\$e=mc^{2}\\$")), } + def tmpl_authors_autocompletion( + self, + element, + relative_curdir, + author_sources_p, + extra_options={}, + extra_fields={}, + ln=CFG_SITE_LANG): + """ + Return the HTML and JavaScript code for the author autocompletion. + """ + + _ = gettext_set_language(ln) + + if relative_curdir: + params = "&relative_curdir=%s" % (relative_curdir,) + else: + params = "" + + # Does this submission allow custom authors or only the suggested ones? + custom_authors_p = extra_options.get("allow_custom_authors", False) + if custom_authors_p: + custom_authors_script = """ +if (typeof datum === "undefined") { + if (typeof document.getElementById('author_textbox').value === "undefined") { + return; + } + datum = {}; + split_author = document.getElementById('author_textbox').value.split(':'); + if (split_author.length != 2){ + alert("If you wish to enter a custom author please use this format: 'Lastname, Firstname: Affiliation'"); + return; + } + try { + datum['lastname'] = split_author[0].split(',')[0].trim(); + datum['firstname'] = split_author[0].split(',')[1].trim(); + datum['affiliation'] = split_author[1].trim(); + } catch(err) { + alert("If you wish to enter a custom author please use this format: 'Lastname, Firstname: Affiliation'"); + return; + } + if (datum['firstname'].length < 1){ + alert("The Initial(s) of the FirstName(s) are missing."); + return; + } + if (datum['lastname'].length < 2){ + alert("The LastName is too short (< 2 characters)."); + return; + } + if (datum['firstname'].indexOf('.') > -1 || datum['firstname'].indexOf('&') > -1 || datum['firstname'].indexOf('#') > -1){ + alert("The Initial(s) of the FirstName(s) contains an illegal character (Period or Dot (.) or Ampersand (&) or hash (#)"); + return; + } + if (datum['lastname'].indexOf('.') > -1 || datum['lastname'].indexOf('&') > -1 || datum['lastname'].indexOf('#') > -1){ + alert("The Initial(s) of the LastName(s) contains an illegal character (Period or Dot (.) or Ampersand (&) or hash (#)"); + return; + } +}""" + custom_authors_text = """ +
+If you wish to enter a custom author please use this format: "Lastname, Firstname: Affiliation"""" + custom_authors_button = """ + +""" + else: + custom_authors_script = """ +if (typeof datum === "undefined") { + if (typeof document.getElementById('author_textbox').value === "undefined") { + return; + } + alert("%s"); +}""" % _("Custom authors are not allowed. Instead, please choose an author from the suggestions.\\nIf you think this is a mistake please contact <%s>" % (CFG_SITE_SUPPORT_EMAIL,)) + custom_authors_text = "" + custom_authors_button = "" + + # Should the "Principal author" label be displayed or not? + show_principal_author_label_p = extra_options.get("highlight_principal_author", True) and "true" or "false" + + # Should the Bloodhound engine be initialized or not? + initialize_engine_p = author_sources_p and "true" or "false" + + # Does this submission have any extra text to display? + extra_text = extra_options.get("extra_text", "") + extra_text = extra_text and "
" + extra_text or extra_text + + # TODO: Move the extra_fields_configuration somewhere more appropriate + extra_fields_configuration = { + # TODO: This value should be coming pre-escpaped within the JSON on MBI + "contribution": { + "label": _("Contribution"), + "html": '
%s: 
', + }, + } + + # Does this submission have any extra fields to add? + extra_fields_rows = "" + extra_fields_import_rows = "" + extra_fields_append_rows = "" + for (extra_field_name, extra_field_p) in extra_fields.iteritems(): + if extra_field_p: + extra_field_configuration = extra_fields_configuration.get(extra_field_name, None) + if extra_field_configuration is not None: + extra_fields_rows += extra_field_configuration["html"] % (extra_field_configuration["label"],) + "\n" + extra_fields_import_rows += "'%s': authors[authorindex]['%s'],\n" % (extra_field_name, extra_field_name,) + extra_fields_append_rows += "'%s': ''," % (extra_field_name,) + + out = """ +
+ + Start by typing an author name and suggestions will become available to choose from. + %(custom_authors_text)s + %(extra_text)s + +
+ +
+ + %(custom_authors_button)s + +
+ +
+
+ + + + + + + + + + + + + + +""" % { + "CFG_SITE_URL": CFG_SITE_URL, + "name": element['name'], + "value": cgi.escape(element['value'], quote=True), + "params": params, + "custom_authors_script": custom_authors_script, + "custom_authors_button": custom_authors_button, + "custom_authors_text": custom_authors_text, + "show_principal_author_label_p": show_principal_author_label_p, + "extra_text": extra_text, + "extra_fields_rows": extra_fields_rows, + "extra_fields_import_rows": extra_fields_import_rows, + "extra_fields_append_rows": extra_fields_append_rows, + "initialize_engine_p": initialize_engine_p, + } + + return out + def displaycplxdoc_displayauthaction(action, linkText): return """ (%(linkText)s)""" % { "action" : action, diff --git a/modules/websubmit/lib/websubmit_unit_tests.py b/modules/websubmit/lib/websubmit_unit_tests.py new file mode 100644 index 0000000000..a507231913 --- /dev/null +++ b/modules/websubmit/lib/websubmit_unit_tests.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014 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. + +"""WebSubmit Unit Test Suite.""" + +__revision__ = "$Id$" + +from invenio.testutils import InvenioTestCase +from invenio.testutils import make_test_suite, run_test_suite + + +class WebSubmitAuthorFunctions(InvenioTestCase): + + def test__convert_record_authors_to_json(self): + from invenio.websubmit_engine import _convert_record_authors_to_json + + expected_output = '{"items": [{"affiliation": "Aachen, Tech. Hochsch.", "name": "Heister, A"}, {"name": "Schael, S"}, {"name": "Barate, R"}, {"name": "Bruneliere, R"}, {"name": "De Bonis, I"}, {"name": "Decamp, D"}, {"name": "Goy, C"}, {"name": "Jezequel, S"}, {"name": "Lees, J P"}, {"name": "Martin, F"}, {"name": "Merle, E"}, {"name": "Minard, M N"}, {"name": "Pietrzyk, B"}, {"name": "Trocme, B"}, {"name": "Boix, G"}, {"name": "Bravo, S"}, {"name": "Casado, M P"}, {"name": "Chmeissani, M"}, {"name": "Crespo, J M"}, {"name": "Fernandez, E"}, {"name": "Fernandez-Bosman, M"}, {"name": "Garrido, L"}, {"name": "Grauges, E"}, {"name": "Lopez, J"}, {"name": "Martinez, M"}, {"name": "Merino, G"}, {"name": "Miquel, R"}, {"name": "Mir, L M"}, {"name": "Pacheco, A"}, {"name": "Paneque, D"}, {"name": "Ruiz, H"}, {"name": "Colaleo, A"}, {"name": "Creanza, D"}, {"name": "De Filippis, N"}, {"name": "De Palma, M"}, {"name": "Iaselli, G"}, {"name": "Maggi, G"}, {"name": "Maggi, M"}, {"name": "Nuzzo, S"}, {"name": "Ranieri, A"}, {"name": "Raso, G"}, {"name": "Ruggieri, F"}, {"name": "Selvaggi, G"}, {"name": "Silvestris, L"}, {"name": "Tempesta, P"}, {"name": "Tricomi, A"}, {"name": "Zito, G"}, {"name": "Huang, X"}, {"name": "Lin, J"}, {"name": "Ouyang, Q"}, {"name": "Wang, T"}, {"name": "Xie, Y"}, {"name": "Xu, R"}, {"name": "Xue, S"}, {"name": "Zhang, J"}, {"name": "Zhang, L"}, {"name": "Zhao, W"}, {"name": "Abbaneo, D"}, {"name": "Azzurri, P"}, {"name": "Barklow, T"}, {"name": "Buchmuller, O"}, {"name": "Cattaneo, M"}, {"name": "Cerutti, F"}, {"name": "Clerbaux, B"}, {"name": "Drevermann, H"}, {"name": "Forty, R W"}, {"name": "Frank, M"}, {"name": "Gianotti, F"}, {"name": "Greening, T C"}, {"name": "Hansen, J B"}, {"name": "Harvey, J"}, {"name": "Hutchcroft, D E"}, {"name": "Janot, P"}, {"name": "Jost, B"}, {"name": "Kado, M"}, {"name": "Maley, P"}, {"name": "Mato, P"}, {"name": "Moutoussi, A"}, {"name": "Ranjard, F"}, {"name": "Rolandi, L"}, {"name": "Schlatter, D"}, {"name": "Sguazzoni, G"}, {"name": "Tejessy, W"}, {"name": "Teubert, F"}, {"name": "Valassi, A"}, {"name": "Videau, I"}, {"name": "Ward, J J"}, {"name": "Badaud, F"}, {"name": "Dessagne, S"}, {"name": "Falvard, A"}, {"name": "Fayolle, D"}, {"name": "Gay, P"}, {"name": "Jousset, J"}, {"name": "Michel, B"}, {"name": "Monteil, S"}, {"name": "Pallin, D"}, {"name": "Pascolo, J M"}, {"name": "Perret, P"}, {"name": "Hansen, J D"}, {"name": "Hansen, J R"}, {"name": "Hansen, P H"}, {"name": "Nilsson, B S"}, {"name": "Waananen, A"}, {"name": "Kyriakis, A"}, {"name": "Markou, C"}, {"name": "Simopoulou, E"}, {"name": "Vayaki, A"}, {"name": "Zachariadou, K"}, {"name": "Blondel, A"}, {"name": "Brient, J C"}, {"name": "Machefert, F P"}, {"name": "Rouge, A"}, {"name": "Swynghedauw, M"}, {"name": "Tanaka, R"}, {"name": "Videau, H L"}, {"name": "Ciulli, V"}, {"name": "Focardi, E"}, {"name": "Parrini, G"}, {"name": "Antonelli, A"}, {"name": "Antonelli, M"}, {"name": "Bencivenni, G"}, {"name": "Bologna, G"}, {"name": "Bossi, F"}, {"name": "Campana, P"}, {"name": "Capon, G"}, {"name": "Chiarella, V"}, {"name": "Laurelli, P"}, {"name": "Mannocchi, G"}, {"name": "Murtas, F"}, {"name": "Murtas, G P"}, {"name": "Passalacqua, L"}, {"name": "Pepe-Altarelli, M"}, {"name": "Spagnolo, P"}, {"name": "Kennedy, J"}, {"name": "Lynch, J G"}, {"name": "Negus, P"}, {"name": "O\'Shea, V"}, {"name": "Smith, D"}, {"name": "Thompson, A S"}, {"name": "Wasserbaech, S R"}, {"name": "Cavanaugh, R"}, {"name": "Dhamotharan, S"}, {"name": "Geweniger, C"}, {"name": "Hanke, P"}, {"name": "Hepp, V"}, {"name": "Kluge, E E"}, {"name": "Leibenguth, G"}, {"name": "Putzer, A"}, {"name": "Stenzel, H"}, {"name": "Tittel, K"}, {"name": "Werner, S"}, {"name": "Wunsch, M"}, {"name": "Beuselinck, R"}, {"name": "Binnie, D M"}, {"name": "Cameron, W"}, {"name": "Davies, G"}, {"name": "Dornan, P J"}, {"name": "Girone, M"}, {"name": "Hill, R D"}, {"name": "Marinelli, N"}, {"name": "Nowell, J"}, {"name": "Przysiezniak, H"}, {"name": "Rutherford, S A"}, {"name": "Sedgbeer, J K"}, {"name": "Thompson, J C"}, {"name": "White, R"}, {"name": "Ghete, V M"}, {"name": "Girtler, P"}, {"name": "Kneringer, E"}, {"name": "Kuhn, D"}, {"name": "Rudolph, G"}, {"name": "Bouhova-Thacker, E"}, {"name": "Bowdery, C K"}, {"name": "Clarke, D P"}, {"name": "Ellis, G"}, {"name": "Finch, A J"}, {"name": "Foster, F"}, {"name": "Hughes, G"}, {"name": "Jones, R W L"}, {"name": "Pearson, M R"}, {"name": "Robertson, N A"}, {"name": "Smizanska, M"}, {"name": "Lema\\u00eetre, V"}, {"name": "Blumenschein, U"}, {"name": "Holldorfer, F"}, {"name": "Jakobs, K"}, {"name": "Kayser, F"}, {"name": "Kleinknecht, K"}, {"name": "Muller, A S"}, {"name": "Quast, G"}, {"name": "Renk, B"}, {"name": "Sander, H G"}, {"name": "Schmeling, S"}, {"name": "Wachsmuth, H"}, {"name": "Zeitnitz, C"}, {"name": "Ziegler, T"}, {"name": "Bonissent, A"}, {"name": "Carr, J"}, {"name": "Coyle, P"}, {"name": "Curtil, C"}, {"name": "Ealet, A"}, {"name": "Fouchez, D"}, {"name": "Leroy, O"}, {"name": "Kachelhoffer, T"}, {"name": "Payre, P"}, {"name": "Rousseau, D"}, {"name": "Tilquin, A"}, {"name": "Ragusa, F"}, {"name": "David, A"}, {"name": "Dietl, H"}, {"name": "Ganis, G"}, {"name": "Huttmann, K"}, {"name": "Lutjens, G"}, {"name": "Mannert, C"}, {"name": "Manner, W"}, {"name": "Moser, H G"}, {"name": "Settles, R"}, {"name": "Wolf, G"}, {"name": "Boucrot, J"}, {"name": "Callot, O"}, {"name": "Davier, M"}, {"name": "Duflot, L"}, {"name": "Grivaz, J F"}, {"name": "Heusse, P"}, {"name": "Jacholkowska, A"}, {"name": "Loomis, C"}, {"name": "Serin, L"}, {"name": "Veillet, J J"}, {"name": "De Vivie de Regie, J B"}, {"name": "Yuan, C"}, {"name": "Bagliesi, G"}, {"name": "Boccali, T"}, {"name": "Fo\\u00e0, L"}, {"name": "Giammanco, A"}, {"name": "Giassi, A"}, {"name": "Ligabue, F"}, {"name": "Messineo, A"}, {"name": "Palla, F"}, {"name": "Sanguinetti, G"}, {"name": "Sciaba, A"}, {"name": "Tenchini, R"}, {"name": "Venturi, A"}, {"name": "Verdini, P G"}, {"name": "Awunor, O"}, {"name": "Blair, G A"}, {"name": "Coles, J"}, {"name": "Cowan, G"}, {"name": "Garc\\u00eda-Bellido, A"}, {"name": "Green, M G"}, {"name": "Jones, L T"}, {"name": "Medcalf, T"}, {"name": "Misiejuk, A"}, {"name": "Strong, J A"}, {"name": "Teixeira-Dias, P"}, {"name": "Clifft, R W"}, {"name": "Edgecock, T R"}, {"name": "Norton, P R"}, {"name": "Tomalin, I R"}, {"name": "Bloch-Devaux, B"}, {"name": "Boumediene, D"}, {"name": "Colas, P"}, {"name": "Fabbro, B"}, {"name": "Lancon, E"}, {"name": "Lemaire, M C"}, {"name": "Locci, E"}, {"name": "Perez, P"}, {"name": "Rander, J"}, {"name": "Renardy, J F"}, {"name": "Rosowsky, A"}, {"name": "Seager, P"}, {"name": "Trabelsi, A"}, {"name": "Tuchming, B"}, {"name": "Vallage, B"}, {"name": "Konstantinidis, N P"}, {"name": "Litke, A M"}, {"name": "Taylor, G"}, {"name": "Booth, C N"}, {"name": "Cartwright, S"}, {"name": "Combley, F"}, {"name": "Hodgson, P N"}, {"name": "Lehto, M H"}, {"name": "Thompson, L F"}, {"name": "Affholderbach, K"}, {"name": "Bohrer, A"}, {"name": "Brandt, S"}, {"name": "Grupen, C"}, {"name": "Hess, J"}, {"name": "Ngac, A"}, {"name": "Prange, G"}, {"name": "Sieler, U"}, {"name": "Borean, C"}, {"name": "Giannini, G"}, {"name": "He, H"}, {"name": "Putz, J"}, {"name": "Rothberg, J E"}, {"name": "Armstrong, S R"}, {"name": "Berkelman, K"}, {"name": "Cranmer, K"}, {"name": "Ferguson, D P S"}, {"name": "Gao, Y"}, {"name": "Gonzalez, S"}, {"name": "Hayes, O J"}, {"name": "Hu, H"}, {"name": "Jin, S"}, {"name": "Kile, J"}, {"name": "McNamara, P A"}, {"name": "Nielsen, J"}, {"name": "Pan, Y B"}, {"name": "Von Wimmersperg-Toller, J H"}, {"name": "Wiedenmann, W"}, {"name": "Wu, J"}, {"name": "Wu, S L"}, {"name": "Wu, X"}, {"name": "Zobernig, G"}, {"name": "Dissertori, G"}]}' + output = _convert_record_authors_to_json(10) + assert expected_output == output + +TEST_SUITE = make_test_suite(WebSubmitAuthorFunctions) + +if __name__ == "__main__": + run_test_suite(TEST_SUITE) diff --git a/modules/websubmit/lib/websubmit_web_tests.py b/modules/websubmit/lib/websubmit_web_tests.py index 09a559d21c..c6397b78c2 100644 --- a/modules/websubmit/lib/websubmit_web_tests.py +++ b/modules/websubmit/lib/websubmit_web_tests.py @@ -18,11 +18,14 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """WebSubmit module web tests.""" + import time from invenio.config import CFG_SITE_SECURE_URL -from invenio.testutils import make_test_suite, \ - run_test_suite, \ - InvenioWebTestCase +from invenio.testutils import ( + make_test_suite, + run_test_suite, + InvenioWebTestCase +) class InvenioWebSubmitWebTest(InvenioWebTestCase): @@ -310,7 +313,73 @@ def test_revise_picture_admin(self): self.page_source_test(expected_text=['Submission Complete!', \ 'Your document has the following reference(s): CERN-GE-9806033']) -TEST_SUITE = make_test_suite(InvenioWebSubmitWebTest, ) + def test_autocompletion_authors(self): + """""" + from time import sleep + from random import randint,choice + from string import ascii_uppercase, digits + def random_string(N=None): + if N == None: + N = randint(10, 25) + return "".join(choice(ascii_uppercase + digits) for _ in range(N)) + test_authors = 10 + self.browser.get(CFG_SITE_SECURE_URL) + self.login(username="admin", password="") + self.browser.get(CFG_SITE_SECURE_URL+"/submit?ln=en&doctype=DEMOTHE") + self.find_element_by_xpath_with_timeout("//input[@value='Submit New Record']") + self.browser.find_element_by_xpath("//input[@value='Submit New Record']").click() + self.find_element_by_name_with_timeout("DEMOTHE_TITLE") + self.fill_textbox(textbox_name="DEMOTHE_TITLE", text=random_string()) + title = self.browser.find_element_by_name("DEMOTHE_TITLE") + self.find_element_by_id_with_timeout("author_textbox") + author_names = {} + authors_to_input = ["Ellis", "Dickinson", "Bach"] + for i in range(0,2): + self.browser.find_element_by_id("author_textbox").send_keys(authors_to_input[i]) + self.find_element_by_id_with_timeout("autocomplete_element_"+str(i+1)) + self.browser.find_element_by_id("autocomplete_element_"+str(i+1)).click() + author_names[i] = self.browser.find_elements_by_class_name("author-row-header-name")[i].text + + self.fill_textbox(textbox_name="DEMOTHE_ABS", text=random_string()) + abstract = self.browser.find_element_by_name("DEMOTHE_ABS").text + self.choose_selectbox_option_by_label("DEMOTHE_LANG", label="English") + self.fill_textbox(textbox_name="DEMOTHE_PUBL", text="CERN") + self.fill_textbox("DEMOTHE_PLDEF", text="Geneva") + self.choose_selectbox_option_by_label(selectbox_name="DEMOTHE_DIPL", label="MSc") + self.fill_textbox(textbox_name="DEMOTHE_DATE", text="11/11/1991") + self.fill_textbox(textbox_name="DEMOTHE_UNIV", text="AUTH") + self.fill_textbox(textbox_name="DEMOTHE_PLACE", text="Thessaloniki") + self.fill_textbox(textbox_name="DEMOTHE_FILE", text="/opt/invenio/lib/webtest/invenio/test.pdf") + author_contributions = {} + for i in range(0, 2): + self.browser.find_elements_by_class_name("author-row-body-extra-contribution")[i].send_keys(random_string()) + for i in range(0, 2): + author_contributions[i] = self.browser.find_elements_by_class_name("author-row-body-extra-contribution")[i].get_attribute("value") + self.find_element_by_name_with_timeout("endS") + self.browser.find_element_by_name("endS").click() + self.find_element_by_xpath_with_timeout("html/body/div[2]/div[3]/form/center/table/tbody/tr[2]/td/small/b[2]") + doc_ref = self.browser.find_element_by_xpath("html/body/div[2]/div[3]/form/center/table/tbody/tr[2]/td/small/b[2]").text + self.browser.get(CFG_SITE_SECURE_URL + "/submit?ln=en&doctype=DEMOTHE") + self.find_element_by_xpath_with_timeout("//input[@value='Modify Record']") + self.browser.find_element_by_xpath("//input[@value='Modify Record']").click() + self.browser.find_element_by_name("DEMOTHE_RN").clear() + self.fill_textbox(textbox_name="DEMOTHE_RN", text=doc_ref) + self.choose_selectbox_option_by_label(selectbox_name="DEMOTHE_CHANGE[]", label="Title") + self.choose_selectbox_option_by_label(selectbox_name="DEMOTHE_CHANGE[]", label="Author(s)") + self.choose_selectbox_option_by_label(selectbox_name="DEMOTHE_CHANGE[]", label="Abstract") + sleep(5) + self.find_element_by_name_with_timeout("endS") + self.browser.find_element_by_name("endS").click() + for i in range(0, 2): + self.find_elements_by_class_name_with_timeout("author-row-header-name") + self.assertEqual(self.browser.find_elements_by_class_name("author-row-header-name")[i].text, author_names[i], "Authors must stay the same") + self.assertEqual( + self.browser.find_elements_by_class_name("author-row-body-extra-contribution")[i].get_attribute("value"), + author_contributions[i], + "Contributions should stay in the same authors" + ) + +TEST_SUITE = make_test_suite(InvenioWebSubmitWebTest,) if __name__ == '__main__': run_test_suite(TEST_SUITE, warn_user=True) diff --git a/modules/websubmit/lib/websubmit_webinterface.py b/modules/websubmit/lib/websubmit_webinterface.py index 3821c52b91..ea567c2500 100644 --- a/modules/websubmit/lib/websubmit_webinterface.py +++ b/modules/websubmit/lib/websubmit_webinterface.py @@ -65,13 +65,18 @@ webstyle_templates = invenio.template.load('webstyle') websearch_templates = invenio.template.load('websearch') -from invenio.websubmit_engine import home, action, interface, endaction, makeCataloguesTable +from invenio.websubmit_engine import home, \ + action, \ + interface, \ + endaction, \ + makeCataloguesTable, \ + get_authors_from_allowed_sources class WebInterfaceSubmitPages(WebInterfaceDirectory): - _exports = ['summary', 'sub', 'direct', '', 'attachfile', 'uploadfile', \ - 'getuploadedfile', 'upload_video', ('continue', 'continue_'), \ - 'doilookup'] + _exports = ['summary', 'sub', 'direct', '', 'attachfile', 'uploadfile', + 'getuploadedfile', 'upload_video', ('continue', 'continue_'), + 'doilookup', 'get_authors'] def uploadfile(self, req, form): """ @@ -997,6 +1002,25 @@ def _index(req, c, ln, doctype, act, startPg, access, # Answer to both /submit/ and /submit __call__ = index + def get_authors(self, req, form): + """ + Simple interface for when there is an ajax call to get + a list of authors that matches the currently typed name + string in the input of the web interface. + """ + + argd = wash_urlargd(form, { + "query": (str, ""), + "relative_curdir": (str, ""), + }) + + (result, error) = get_authors_from_allowed_sources( + req, + argd["query"], + argd["relative_curdir"], + ) + + return json.dumps(result) # def retrieve_most_recent_attached_file(file_path): # """ From 84c044c79dc68c1ea2c7b41f263d667fbd2816fc Mon Sep 17 00:00:00 2001 From: Avraam Tsantekidis Date: Mon, 28 Jul 2014 10:49:58 +0200 Subject: [PATCH 41/83] WebComment: general comment format improvements * FEATURE Introduces the CFG_WEBCOMMENT_ENABLE_HTML_EMAILS config variable. When True, emails will also include HTML content. * FEATURE Introduces the body_format column for cmtRECORDCOMMENT that describes the format of the body of each comment ("HTML", "TEXT", etc.). * Handles comments appropriately when storing and displaying them, according to their body format and desired output format. (closes #2064) Co-Authored-By: Nikolaos Kasioumis Signed-off-by: Nikolaos Kasioumis --- config/invenio.conf | 9 + modules/miscutil/lib/htmlutils.py | 48 +- ...07_30_webcomment_new_column_body_format.py | 107 ++++ modules/miscutil/sql/tabcreate.sql | 1 + modules/webcomment/lib/webcomment.py | 532 ++++++++++++------ modules/webcomment/lib/webcomment_config.py | 20 +- .../webcomment/lib/webcomment_templates.py | 465 ++++++++++++--- .../webcomment/lib/webcomment_unit_tests.py | 84 ++- modules/webcomment/lib/webcommentadminlib.py | 15 +- .../webmessage/lib/webmessage_mailutils.py | 122 +++- modules/webstyle/css/invenio.css | 6 + .../webstyle/etc/invenio-ckeditor-config.js | 31 +- .../webstyle/etc/invenio-ckeditor-content.css | 28 +- requirements.txt | 3 + 14 files changed, 1167 insertions(+), 304 deletions(-) create mode 100644 modules/miscutil/lib/upgrades/invenio_2014_07_30_webcomment_new_column_body_format.py diff --git a/config/invenio.conf b/config/invenio.conf index 408411ba6a..51a4826a7e 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -1475,6 +1475,15 @@ CFG_WEBCOMMENT_MAX_ATTACHED_FILES = 5 # discussions. CFG_WEBCOMMENT_MAX_COMMENT_THREAD_DEPTH = 1 +# CFG_WEBCOMMENT_ENABLE_HTML_EMAILS -- if True, emails will also contain +# HTML content, in addition to the plaintext version. +CFG_WEBCOMMENT_ENABLE_HTML_EMAILS = True + +# CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING -- if True, and when +# CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR is False, plain text will be rendered +# as Markdown . +CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING = True + ################################## # Part 11: BibSched parameters ## ################################## diff --git a/modules/miscutil/lib/htmlutils.py b/modules/miscutil/lib/htmlutils.py index fbeeb3366b..7f158a5bf4 100644 --- a/modules/miscutil/lib/htmlutils.py +++ b/modules/miscutil/lib/htmlutils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# + # This file is part of Invenio. # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2013 CERN. # @@ -16,6 +16,7 @@ # 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. + """HTML utilities.""" __revision__ = "$Id$" @@ -595,6 +596,51 @@ def get_html_text_editor(name, id=None, content='', textual_content=None, width= evt.editor.resetDirty(); } ); /* End workaround */ + + // Catch any key being pressed + evt.editor.on('key', function(e) { + + /* + Adding inline text can be difficult due to problebatic + blockquote breaking. The code below will catch the "Enter" + key being pressed and will try to break the blockquotes. + The following code has partially been taken from: + + */ + if ( e.data.keyCode == 13 ) { + + // The following will break all blockquotes, one Enter at a time + var selection = oEditor.getSelection(); + var element = selection.getStartElement(); + var parent = element.getParent(); + var range_split_block = true; + + if ( element.is("blockquote") && parent.is("body") ) { + if ( element.getText().trim() == "" ) { + element.remove(); + range_split_block = false; + // Adding an empty paragraph seems to make it smoother + CKEDITOR.instances.msg.insertHtml("

"); + } + } + + if ( range_split_block == true ) { + var ranges = selection.getRanges(true); + for ( var i = ranges.length - 1 ; i > 0 ; i-- ) { + ranges[i].deleteContents(); + } + var range = ranges[0]; + range.splitBlock("blockquote"); + if ( ! ( element.is("p") && parent.is("body") ) ) { + // Adding an empty paragraph seems to make it smoother + CKEDITOR.instances.msg.insertHtml("

"); + } + } + + } + + }); + }) //]]> diff --git a/modules/miscutil/lib/upgrades/invenio_2014_07_30_webcomment_new_column_body_format.py b/modules/miscutil/lib/upgrades/invenio_2014_07_30_webcomment_new_column_body_format.py new file mode 100644 index 0000000000..7e4fc17257 --- /dev/null +++ b/modules/miscutil/lib/upgrades/invenio_2014_07_30_webcomment_new_column_body_format.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2014 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 +from invenio.webcomment_config import CFG_WEBCOMMENT_BODY_FORMATS +from invenio.webmessage_mailutils import email_quoted_txt2html + +depends_on = ['invenio_release_1_1_0'] + + +def info(): + return "New column 'body_format' for WebComment's cmtRECORDCOMMENT." + + +def do_upgrade(): + # First, insert the new column in the table. + cmtRECORDCOMMENT_definition = run_sql("SHOW CREATE TABLE cmtRECORDCOMMENT")[0][1] + if "body_format" not in cmtRECORDCOMMENT_definition: + run_sql("""ALTER TABLE cmtRECORDCOMMENT + ADD COLUMN body_format VARCHAR(10) NOT NULL DEFAULT %s + AFTER body;""", + (CFG_WEBCOMMENT_BODY_FORMATS["TEXT"],) + ) + + number_of_comments = run_sql("""SELECT COUNT(id) + FROM cmtRECORDCOMMENT""")[0][0] + + if number_of_comments > 0: + + # NOTE: Consider that the bigger the number of comments, + # the more powerful the server. Keep the number of + # batches fixed and scale the batch size instead. + number_of_select_batches = 100 + + select_batch_size = \ + number_of_comments >= (number_of_select_batches * number_of_select_batches) and \ + number_of_comments / number_of_select_batches or \ + number_of_comments + + number_of_select_iterations = \ + number_of_select_batches + \ + (number_of_comments % select_batch_size and 1) + + comments_select_query = """ SELECT id, + body, + body_format + FROM cmtRECORDCOMMENT + LIMIT %s, %s""" + + comments_update_query = """ UPDATE cmtRECORDCOMMENT + SET body = %s, + body_format = %s + WHERE id = %s""" + + for number_of_select_iteration in xrange(number_of_select_iterations): + + comments = run_sql( + comments_select_query, + (number_of_select_iteration * select_batch_size, + select_batch_size) + ) + + for (comment_id, comment_body, comment_body_format) in comments: + + if comment_body_format == CFG_WEBCOMMENT_BODY_FORMATS["TEXT"]: + + comment_body = email_quoted_txt2html( + comment_body, + indent_html=("
", "
") + ) + + run_sql( + comments_update_query, + (comment_body, + CFG_WEBCOMMENT_BODY_FORMATS["HTML"], + comment_id) + ) + + +def estimate(): + # TODO: The estimated time needed depends on the size of the table. + # Should we calculate this more accurately? + return 1 + + +def pre_upgrade(): + pass + + +def post_upgrade(): + pass diff --git a/modules/miscutil/sql/tabcreate.sql b/modules/miscutil/sql/tabcreate.sql index 648f3fe4e2..04af138884 100644 --- a/modules/miscutil/sql/tabcreate.sql +++ b/modules/miscutil/sql/tabcreate.sql @@ -3870,6 +3870,7 @@ CREATE TABLE IF NOT EXISTS cmtRECORDCOMMENT ( id_user int(15) unsigned NOT NULL default '0', title varchar(255) NOT NULL default '', body text NOT NULL default '', + body_format varchar(10) NOT NULL default 'TXT', date_creation datetime NOT NULL default '0000-00-00 00:00:00', star_score tinyint(5) unsigned NOT NULL default '0', nb_votes_yes int(10) NOT NULL default '0', diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index ce92ec10f0..03d66280a8 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -29,33 +29,39 @@ import cgi import re from datetime import datetime, timedelta +from bleach import clean, linkify # Invenio imports: from invenio.dbquery import run_sql -from invenio.config import CFG_PREFIX, \ - CFG_SITE_LANG, \ - CFG_WEBALERT_ALERT_ENGINE_EMAIL,\ - CFG_SITE_SUPPORT_EMAIL,\ - CFG_WEBCOMMENT_ALERT_ENGINE_EMAIL,\ - CFG_SITE_URL,\ - CFG_SITE_NAME,\ - CFG_WEBCOMMENT_ALLOW_REVIEWS,\ - CFG_WEBCOMMENT_ALLOW_SHORT_REVIEWS,\ - CFG_WEBCOMMENT_ALLOW_COMMENTS,\ - CFG_WEBCOMMENT_ADMIN_NOTIFICATION_LEVEL,\ - CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN,\ - CFG_WEBCOMMENT_TIMELIMIT_PROCESSING_COMMENTS_IN_SECONDS,\ - CFG_WEBCOMMENT_DEFAULT_MODERATOR, \ - CFG_SITE_RECORD, \ - CFG_WEBCOMMENT_EMAIL_REPLIES_TO, \ - CFG_WEBCOMMENT_ROUND_DATAFIELD, \ - CFG_WEBCOMMENT_RESTRICTION_DATAFIELD, \ - CFG_WEBCOMMENT_MAX_COMMENT_THREAD_DEPTH -from invenio.webmessage_mailutils import \ - email_quote_txt, \ - email_quoted_txt2html -from invenio.htmlutils import tidy_html +from invenio.config import \ + CFG_PREFIX, \ + CFG_SITE_LANG, \ + CFG_WEBALERT_ALERT_ENGINE_EMAIL,\ + CFG_SITE_SUPPORT_EMAIL,\ + CFG_WEBCOMMENT_ALERT_ENGINE_EMAIL,\ + CFG_SITE_URL,\ + CFG_SITE_SECURE_URL,\ + CFG_SITE_NAME,\ + CFG_WEBCOMMENT_ALLOW_REVIEWS,\ + CFG_WEBCOMMENT_ALLOW_SHORT_REVIEWS,\ + CFG_WEBCOMMENT_ALLOW_COMMENTS,\ + CFG_WEBCOMMENT_ADMIN_NOTIFICATION_LEVEL,\ + CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN,\ + CFG_WEBCOMMENT_TIMELIMIT_PROCESSING_COMMENTS_IN_SECONDS,\ + CFG_WEBCOMMENT_DEFAULT_MODERATOR, \ + CFG_SITE_RECORD, \ + CFG_WEBCOMMENT_EMAIL_REPLIES_TO, \ + CFG_WEBCOMMENT_ROUND_DATAFIELD, \ + CFG_WEBCOMMENT_RESTRICTION_DATAFIELD, \ + CFG_WEBCOMMENT_MAX_COMMENT_THREAD_DEPTH, \ + CFG_WEBCOMMENT_ENABLE_HTML_EMAILS, \ + CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, \ + CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING +from invenio.webmessage_mailutils import email_quote_txt +from invenio.htmlutils import \ + CFG_HTML_BUFFER_ALLOWED_TAG_WHITELIST, \ + CFG_HTML_BUFFER_ALLOWED_ATTRIBUTE_WHITELIST from invenio.webuser import get_user_info, get_email, collect_user_info from invenio.dateutils import convert_datetext_to_dategui, \ datetext_default, \ @@ -64,9 +70,12 @@ from invenio.errorlib import register_exception from invenio.messages import wash_language, gettext_set_language from invenio.urlutils import wash_url_argument -from invenio.webcomment_config import CFG_WEBCOMMENT_ACTION_CODE, \ - InvenioWebCommentError, \ - InvenioWebCommentWarning +from invenio.webcomment_config import \ + CFG_WEBCOMMENT_ACTION_CODE, \ + CFG_WEBCOMMENT_BODY_FORMATS, \ + CFG_WEBCOMMENT_OUTPUT_FORMATS, \ + InvenioWebCommentError, \ + InvenioWebCommentWarning from invenio.access_control_engine import acc_authorize_action from invenio.search_engine import \ guess_primary_collection_of_a_record, \ @@ -74,14 +83,12 @@ get_collection_reclist, \ get_colID from invenio.search_engine_utils import get_fieldvalues -from invenio.webcomment_washer import EmailWasher try: import invenio.template webcomment_templates = invenio.template.load('webcomment') except: pass - def perform_request_display_comments_or_remarks(req, recID, display_order='od', display_since='all', nb_per_page=100, page=1, ln=CFG_SITE_LANG, voted=-1, reported=-1, subscribed=0, reviews=0, uid=-1, can_send_comments=False, can_attach_files=False, user_is_subscribed_to_discussion=False, user_can_unsubscribe_from_discussion=False, display_comment_rounds=None): """ Returns all the comments (reviews) of a specific internal record or external basket record. @@ -473,6 +480,7 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): id_bibrec, id_user, cmt_body, + cmt_body_format, cmt_date, cmt_star, cmt_vote, cmt_nb_votes_total, @@ -504,7 +512,9 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): %(review_stuff)s body = ---start body--- + %(cmt_body)s + ---end body--- Please go to the record page %(comment_admin_link)s to delete this message if necessary. A warning will be sent to the user in question.''' % \ @@ -523,7 +533,7 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): 'cmt_reported' : cmt_reported, 'review_stuff' : CFG_WEBCOMMENT_ALLOW_REVIEWS and \ "star score\t= %s\n\treview title\t= %s" % (cmt_star, cmt_title) or "", - 'cmt_body' : cmt_body, + 'cmt_body' : webcomment_templates.tmpl_prepare_comment_body(cmt_body, cmt_body_format, CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"]), 'comment_admin_link' : CFG_SITE_URL + "/"+ CFG_SITE_RECORD +"/" + str(id_bibrec) + '/comments#' + str(cmt_id), 'user_admin_link' : "user_admin_link" #! FIXME } @@ -531,7 +541,72 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): #FIXME to be added to email when websession module is over: #If you wish to ban the user, you can do so via the User Admin Panel %(user_admin_link)s. - send_email(from_addr, to_addrs, subject, body) + if CFG_WEBCOMMENT_ENABLE_HTML_EMAILS: + html_content = """ +The following comment has been reported a total of %(cmt_reported)s times. + +

Author:

+
    +
  • nickname = %(nickname)s
  • +
  • email = <%(user_email)s>
  • +
  • user_id = %(uid)s
  • +
  • This user has:

    +
      +
    • total number of reports = %(user_nb_abuse_reports)s
    • + %(votes)s +
    +
+ +

Comment:

+
    +
  • comment_id = %(cmt_id)s
  • +
  • record_id = %(id_bibrec)s
  • +
  • date written = %(cmt_date)s
  • +
  • nb reports = %(cmt_reported)s
  • + %(review_stuff)s +
  • body =
  • +
+ +<--------------->

+ +%(cmt_body)s + +
<--------------->
+ +

Please go to the record page <%(comment_admin_link)s> to delete this message if necessary. + A warning will be sent to the user in question.

""" % { + 'cfg-report_max' : CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN, + 'nickname' : cgi.escape(nickname), + 'user_email' : user_email, + 'uid' : id_user, + 'user_nb_abuse_reports' : user_nb_abuse_reports, + 'user_votes' : user_votes, + 'votes' : CFG_WEBCOMMENT_ALLOW_REVIEWS and \ + "
  • total number of positive votes = %s
  • \n
  • total number of negative votes= %s
  • " % \ + (user_votes, (user_nb_votes_total - user_votes)) + or "", + 'cmt_id' : cmt_id, + 'id_bibrec' : id_bibrec, + 'cmt_date' : cmt_date, + 'cmt_reported' : cmt_reported, + 'review_stuff' : CFG_WEBCOMMENT_ALLOW_REVIEWS and \ + "
  • star score = %s
  • \n
  • review title = %s
  • " % (cmt_star, cmt_title) + or "", + 'cmt_body' : webcomment_templates.tmpl_prepare_comment_body(cmt_body, cmt_body_format, CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"]), + 'comment_admin_link' : CFG_SITE_URL + "/"+ CFG_SITE_RECORD +"/" + str(id_bibrec) + '/comments#' + str(cmt_id), + 'user_admin_link' : "user_admin_link", #! FIXME +} + else: + html_content = None + + return int(send_email( + fromaddr=from_addr, + toaddr=to_addrs, + subject=subject, + content=body, + html_content=html_content + )) + return 1 def check_user_can_report(cmt_id, client_ip_address, uid=-1): @@ -607,6 +682,7 @@ def query_get_comment(comID): id_bibrec, id_user, body, + body_format, DATE_FORMAT(date_creation, '%%Y-%%m-%%d %%H:%%i:%%s'), star_score, nb_votes_yes, @@ -727,7 +803,8 @@ def query_retrieve_comments_or_remarks(recID, display_order='od', display_since= %(ranking)s cmt.id, cmt.round_name, cmt.restriction, - %(reply_to_column)s + %(reply_to_column)s, + cmt.body_format FROM cmtRECORDCOMMENT cmt LEFT JOIN user ON user.id=cmt.id_user WHERE cmt.id_bibrec=%%s @@ -867,17 +944,21 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", @param reply_to: the id of the comment we are replying to with this inserted comment. @return: integer >0 representing id if successful, integer 0 if not """ + current_date = calculate_start_date('0d') - #change utf-8 message into general unicode - msg = msg.decode('utf-8') - note = note.decode('utf-8') - #change general unicode back to utf-8 - msg = msg.encode('utf-8') - note = note.encode('utf-8') + + # NOTE: Change utf-8 message into general unicode and back to utf-8 + # (Why do we do this here?) + msg = msg.decode('utf-8').encode('utf-8') + note = note.decode('utf-8').encode('utf-8') + msg_original = msg + (restriction, round_name) = get_record_status(recID) + if attached_files is None: attached_files = {} + if reply_to and CFG_WEBCOMMENT_MAX_COMMENT_THREAD_DEPTH >= 0: # Check that we have not reached max depth comment_ancestors = get_comment_ancestors(reply_to) @@ -889,48 +970,32 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", # Inherit restriction and group/round of 'parent' comment = query_get_comment(reply_to) if comment: - (round_name, restriction) = comment[10:12] - if editor_type == 'ckeditor': - # Here we remove the line feeds introduced by CKEditor (they - # have no meaning for the user) and replace the HTML line - # breaks by linefeeds, so that we are close to an input that - # would be done without the CKEditor. That's much better if a - # reply to a comment is made with a browser that does not - # support CKEditor. - msg = msg.replace('\n', '').replace('\r', '') - - # We clean the quotes that could have been introduced by - # CKEditor when clicking the 'quote' button, as well as those - # that we have introduced when quoting the original message. - # We can however not use directly '>>' chars to quote, as it - # will be washed/fixed when calling tidy_html(): double-escape - # all > first, and use >> - msg = msg.replace('>', '&gt;') - msg = re.sub('^\s* \s*<(p|div).*?>', '>>', msg) - msg = re.sub('\s*', '', msg) - # Then definitely remove any blockquote, whatever it is - msg = re.sub('', '
    ', msg) - msg = re.sub('', '
    ', msg) - # Tidy up the HTML - msg = tidy_html(msg) - # We remove EOL that might have been introduced when tidying - msg = msg.replace('\n', '').replace('\r', '') - # Now that HTML has been cleaned, unescape > - msg = msg.replace('>', '>') - msg = msg.replace('&gt;', '>') - msg = re.sub('
    )', '\n', msg) - msg = msg.replace(' ', ' ') - # In case additional

    or

    got inserted, interpret - # these as new lines (with a sad trick to do it only once) - # (note that it has been deactivated, as it is messing up - # indentation with >>) - #msg = msg.replace('
    <', '
    \n<') - #msg = msg.replace('

    <', '

    \n<') + (round_name, restriction) = comment[11:13] + + if editor_type == "ckeditor": + msg = linkify(msg) + msg = clean( + msg, + tags=CFG_HTML_BUFFER_ALLOWED_TAG_WHITELIST, + attributes=CFG_HTML_BUFFER_ALLOWED_ATTRIBUTE_WHITELIST, + strip=True + ) + body_format = CFG_WEBCOMMENT_BODY_FORMATS["HTML"] + + elif editor_type == "textarea": + if CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING: + body_format = CFG_WEBCOMMENT_BODY_FORMATS["MARKDOWN"] + else: + body_format = CFG_WEBCOMMENT_BODY_FORMATS["TEXT"] + + else: + # NOTE: it should really be one of the above 2 types. + body_format = CFG_WEBCOMMENT_BODY_FORMATS["TEXT"] query = """INSERT INTO cmtRECORDCOMMENT (id_bibrec, id_user, body, + body_format, date_creation, star_score, nb_votes_total, @@ -938,8 +1003,8 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", round_name, restriction, in_reply_to_id_cmtRECORDCOMMENT) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""" - params = (recID, uid, msg, current_date, score, 0, note, round_name, restriction, reply_to or 0) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""" + params = (recID, uid, msg, body_format, current_date, score, 0, note, round_name, restriction, reply_to or 0) res = run_sql(query, params) if res: new_comid = int(res) @@ -968,7 +1033,7 @@ def notify_subscribers_callback(data): @param data: contains the necessary parameters in a tuple: (recid, uid, comid, msg, note, score, editor_type, reviews) """ - recid, uid, comid, msg, note, score, editor_type, reviews = data + recid, uid, comid, msg, body_format, note, score, editor_type, reviews = data # Email this comment to 'subscribers' (subscribers_emails1, subscribers_emails2) = \ get_users_subscribed_to_discussion(recid) @@ -976,12 +1041,13 @@ def notify_subscribers_callback(data): emails1=subscribers_emails1, emails2=subscribers_emails2, comID=comid, msg=msg, + body_format=body_format, note=note, score=score, editor_type=editor_type, uid=uid) # Register our callback to notify subscribed people after # having replied to our current user. - data = (recID, uid, res, msg, note, score, editor_type, reviews) + data = (recID, uid, res, msg, body_format, note, score, editor_type, reviews) if req: req.register_cleanup(notify_subscribers_callback, data) else: @@ -1152,6 +1218,7 @@ def get_users_subscribed_to_discussion(recID, check_authorizations=True): def email_subscribers_about_new_comment(recID, reviews, emails1, emails2, comID, msg="", + body_format=CFG_WEBCOMMENT_BODY_FORMATS["HTML"], note="", score=0, editor_type='textarea', ln=CFG_SITE_LANG, uid=-1): @@ -1170,11 +1237,11 @@ def email_subscribers_about_new_comment(recID, reviews, emails1, @rtype: bool @return: True if email was sent okay, False if it was not. """ + _ = gettext_set_language(ln) if not emails1 and not emails2: return 0 - # Get title titles = get_fieldvalues(recID, "245__a") if not titles: @@ -1204,67 +1271,101 @@ def email_subscribers_about_new_comment(recID, reviews, emails1, {'report_number': report_numbers and ('[' + report_numbers[0] + '] ') or '', 'title': title} - washer = EmailWasher() - msg = washer.wash(msg) - msg = msg.replace('>>', '>') - email_content = msg if note: - email_content = note + email_content - - # Send emails to people who can unsubscribe - email_header = webcomment_templates.tmpl_email_new_comment_header(recID, - title, - reviews, - comID, - report_numbers, - can_unsubscribe=True, - ln=ln, - uid=uid) - - email_footer = webcomment_templates.tmpl_email_new_comment_footer(recID, - title, - reviews, - comID, - report_numbers, - can_unsubscribe=True, - ln=ln) + email_content = note + msg + else: + email_content = msg + + def send_email_kwargs( + recID=recID, + title=title, + reviews=reviews, + comID=comID, + report_numbers=report_numbers, + can_unsubscribe=True, + uid=uid, + ln=ln, + body_format=body_format, + fromaddr=CFG_WEBCOMMENT_ALERT_ENGINE_EMAIL, + toaddr=[], + subject=email_subject, + content=email_content + ): + + tmpl_email_new_comment_footer_kwargs = { + "recID" : recID, + "title" : title, + "reviews" : reviews, + "comID" : comID, + "report_numbers" : report_numbers, + "can_unsubscribe" : can_unsubscribe, + "ln" : ln, + "html_p" : False, + } + + tmpl_email_new_comment_header_kwargs = tmpl_email_new_comment_footer_kwargs.copy() + tmpl_email_new_comment_header_kwargs.update({ + "uid" : uid, + }) + + kwargs = { + "fromaddr" : fromaddr, + "toaddr" : toaddr, + "subject" : subject, + "content" : webcomment_templates.tmpl_prepare_comment_body( + content, + body_format, + CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"] + ), + "header" : webcomment_templates.tmpl_email_new_comment_header(**tmpl_email_new_comment_header_kwargs), + "footer" : webcomment_templates.tmpl_email_new_comment_footer(**tmpl_email_new_comment_footer_kwargs), + "html_content" : "", + "html_header" : None, + "html_footer" : None, + "ln" : ln, + } + + if CFG_WEBCOMMENT_ENABLE_HTML_EMAILS: + tmpl_email_new_comment_footer_kwargs.update({ + "html_p" : True, + }) + tmpl_email_new_comment_header_kwargs.update({ + "html_p" : True, + }) + kwargs.update({ + "html_content" : webcomment_templates.tmpl_prepare_comment_body( + content, + body_format, + CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"] + ), + "html_header" : webcomment_templates.tmpl_email_new_comment_header(**tmpl_email_new_comment_header_kwargs), + "html_footer" : webcomment_templates.tmpl_email_new_comment_footer(**tmpl_email_new_comment_footer_kwargs), + }) + + return kwargs + + # First, send emails to people who can unsubscribe. res1 = True if emails1: - res1 = send_email(fromaddr=CFG_WEBCOMMENT_ALERT_ENGINE_EMAIL, - toaddr=emails1, - subject=email_subject, - content=email_content, - header=email_header, - footer=email_footer, - ln=ln) - - # Then send email to people who have been automatically - # subscribed to the discussion (they cannot unsubscribe) - email_header = webcomment_templates.tmpl_email_new_comment_header(recID, - title, - reviews, - comID, - report_numbers, - can_unsubscribe=False, - ln=ln, - uid=uid) - - email_footer = webcomment_templates.tmpl_email_new_comment_footer(recID, - title, - reviews, - comID, - report_numbers, - can_unsubscribe=False, - ln=ln) + res1 = send_email( + **send_email_kwargs( + can_unsubscribe=True, + body_format=body_format, + toaddr=emails1 + ) + ) + + # Then, send emails to people who have been automatically subscribed + # to the discussion and cannot unsubscribe. res2 = True if emails2: - res2 = send_email(fromaddr=CFG_WEBCOMMENT_ALERT_ENGINE_EMAIL, - toaddr=emails2, - subject=email_subject, - content=email_content, - header=email_header, - footer=email_footer, - ln=ln) + res2 = send_email( + **send_email_kwargs( + can_unsubscribe=False, + body_format=body_format, + toaddr=emails2 + ) + ) return res1 and res2 @@ -1558,7 +1659,9 @@ def perform_request_add_comment_or_remark(recID=0, - html add form if action is display or reply - html successful added form if action is submit """ + _ = gettext_set_language(ln) + if warnings is None: warnings = [] @@ -1601,6 +1704,7 @@ def perform_request_add_comment_or_remark(recID=0, #errors.append(('ERR_WEBCOMMENT_COMMENTS_NOT_ALLOWED',)) elif action == 'REPLY': + if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: try: raise InvenioWebCommentError(_('Cannot reply to a review.')) @@ -1610,6 +1714,7 @@ def perform_request_add_comment_or_remark(recID=0, return body #errors.append(('ERR_WEBCOMMENT_REPLY_REVIEW',)) return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, nickname, ln, msg, score, note, warnings, can_attach_files=can_attach_files) + elif not reviews and CFG_WEBCOMMENT_ALLOW_COMMENTS: textual_msg = msg if comID > 0: @@ -1617,33 +1722,53 @@ def perform_request_add_comment_or_remark(recID=0, if comment: user_info = get_user_info(comment[2]) if user_info: - date_creation = convert_datetext_to_dategui(str(comment[4])) - # Build two msg: one mostly textual, the other one with HTML markup, for the CkEditor. - msg = _("%(x_name)s wrote on %(x_date)s:")% {'x_name': user_info[2], 'x_date': date_creation} - textual_msg = msg - # 1 For CkEditor input - msg += '\n\n' - msg += comment[3] - msg = email_quote_txt(text=msg) - # Now that we have a text-quoted version, transform into - # something that CkEditor likes, using
    that - # do still enable users to insert comments inline - msg = email_quoted_txt2html(text=msg, - indent_html=('
    ', '  
    '), - linebreak_html=" 
    ", - indent_block=False) - # Add some space for users to easily add text - # around the quoted message - msg = '
    ' + msg + '
    ' - # Due to how things are done, we need to - # escape the whole msg again for the editor - msg = cgi.escape(msg) - - # 2 For textarea input - textual_msg += "\n\n" - textual_msg += comment[3] - textual_msg = email_quote_txt(text=textual_msg) + date_creation = convert_datetext_to_dategui(str(comment[5])) + user_wrote_on = _("%(x_name)s wrote on %(x_date)s:")% {'x_name': user_info[2], 'x_date': date_creation} + + # We want to produce 2 messages here: + # 1. for a rich HTML editor such as CKEditor (msg) + # 2. for a simple HTML textarea (textual_msg) + + msg = """ +

    %s

    +
    + %s +
    +

    + """ % ( + user_wrote_on, + webcomment_templates.tmpl_prepare_comment_body( + comment[3], + comment[4], + CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["CKEDITOR"] + ) + ) + + textual_msg = "%s\n\n%s\n\n" % ( + user_wrote_on, + email_quote_txt( + webcomment_templates.tmpl_prepare_comment_body( + comment[3], + comment[4], + CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["TEXTAREA"] + ), + # TODO: Maybe always use a single ">" for quotations? + indent_txt=">", + # If we are using CKEditor, then we need to escape + # the textual message before passing it to the + # editor. "email_quote_txt" can do that for us. + # Normally, we could check the "editor_type" + # argument that was passed to this function. + # However, it is usually an empty string since + # it is comming from the "add" interface. + # Instead let's directly use the config variable. + #escape_p=(editor_type == "ckeditor") + escape_p=CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR + ) + ) + return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, textual_msg, can_attach_files=can_attach_files, reply_to=comID) + else: try: raise InvenioWebCommentError(_('Comments on records have been disallowed by the administrator.')) @@ -1756,6 +1881,7 @@ def notify_admin_of_new_comment(comID): id_bibrec, id_user, body, + body_format, date_creation, star_score, nb_votes_yes, nb_votes_total, title, @@ -1774,12 +1900,6 @@ def notify_admin_of_new_comment(comID): Star score = %s Title = %s''' % (star_score, title) - washer = EmailWasher() - try: - body = washer.wash(body) - except: - body = cgi.escape(body) - record_info = webcomment_templates.tmpl_email_new_comment_admin(id_bibrec) out = ''' The following %(comment_or_review)s has just been posted (%(date)s). @@ -1796,11 +1916,14 @@ def notify_admin_of_new_comment(comID): %(comment_or_review_caps)s: %(comment_or_review)s ID = %(comID)s %(review_stuff)s + URL = <%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/%(comments_or_reviews)s/#C%(comID)s> Body = <---------------> + %(body)s <---------------> + ADMIN OPTIONS: To moderate the %(comment_or_review)s go to %(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/%(comments_or_reviews)s/display?%(arguments)s ''' % \ @@ -1815,12 +1938,63 @@ def notify_admin_of_new_comment(comID): 'record_details' : record_info, 'comID' : comID2, 'review_stuff' : star_score > 0 and review_stuff or "", - 'body' : body.replace('
    ','\n'), - 'siteurl' : CFG_SITE_URL, - 'CFG_SITE_RECORD' : CFG_SITE_RECORD, + 'body' : webcomment_templates.tmpl_prepare_comment_body(body, body_format, CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"]), + 'siteurl' : CFG_SITE_SECURE_URL, + 'CFG_SITE_RECORD' : CFG_SITE_RECORD, 'arguments' : 'ln=en&do=od#%s' % comID } + if CFG_WEBCOMMENT_ENABLE_HTML_EMAILS: + record_info = webcomment_templates.tmpl_email_new_comment_admin(id_bibrec, html_p=True) + html_content = """ +The following %(comment_or_review)s has just been posted (%(date)s). + +

    AUTHOR:

    +
      +
    • Nickname = %(nickname)s
    • +
    • Email = <%(email)s>
    • +
    • User ID = %(uid)s
    • +
    + +

    RECORD CONCERNED:

    + + +

    %(comment_or_review_caps)s:

    + + +<--------------->

    +%(body)s +
    <--------------->
    + +
    ADMIN OPTIONS: +
    To moderate the %(comment_or_review)s go to <%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/%(comments_or_reviews)s/display?%(arguments)s>""" % { + 'comment_or_review' : star_score > 0 and 'review' or 'comment', + 'comment_or_review_caps': star_score > 0 and 'REVIEW' or 'COMMENT', + 'comments_or_reviews' : star_score > 0 and 'reviews' or 'comments', + 'date' : date_creation, + 'nickname' : nickname, + 'email' : email, + 'uid' : id_user, + 'recID' : id_bibrec, + 'record_details' : record_info, + 'comID' : comID2, + 'review_stuff' : star_score > 0 and review_stuff or "", + 'body' : webcomment_templates.tmpl_prepare_comment_body(body, body_format, CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"]), + 'siteurl' : CFG_SITE_SECURE_URL, + 'CFG_SITE_RECORD' : CFG_SITE_RECORD, + 'arguments' : 'ln=en&do=od#%s' % comID +} + else: + html_content = None + from_addr = '%s WebComment <%s>' % (CFG_SITE_NAME, CFG_WEBALERT_ALERT_ENGINE_EMAIL) comment_collection = get_comment_collection(comID) to_addrs = get_collection_moderators(comment_collection) @@ -1831,7 +2005,13 @@ def notify_admin_of_new_comment(comID): report_nums = ', '.join(report_nums) subject = "A new comment/review has just been posted [%s|%s]" % (rec_collection, report_nums) - send_email(from_addr, to_addrs, subject, out) + send_email( + fromaddr=from_addr, + toaddr=to_addrs, + subject=subject, + content=out, + html_content=html_content + ) def check_recID_is_in_range(recID, warnings=[], ln=CFG_SITE_LANG): """ @@ -1987,7 +2167,7 @@ def check_user_can_view_comment(user_info, comid, restriction=None): if restriction is None: comment = query_get_comment(comid) if comment: - restriction = comment[11] + restriction = comment[12] else: return (1, 'Comment %i does not exist' % comid) if restriction == "": @@ -2160,9 +2340,9 @@ def perform_display_your_comments(user_info, elif selected_order_by_option == "ocf": query_params += " ORDER BY date_creation ASC" elif selected_order_by_option == "grlf": - query = "SELECT cmt.id_bibrec, cmt.id, cmt.date_creation, cmt.body, cmt.status, cmt.in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT as cmt left join (SELECT max(date_creation) as maxdatecreation, id_bibrec FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0 GROUP BY id_bibrec) as grp on cmt.id_bibrec = grp.id_bibrec WHERE id_user=%s AND star_score = 0 ORDER BY grp.maxdatecreation DESC, cmt.date_creation DESC" + query = "SELECT cmt.id_bibrec, cmt.id, cmt.date_creation, cmt.body, cmt.body_format, cmt.status, cmt.in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT as cmt left join (SELECT max(date_creation) as maxdatecreation, id_bibrec FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0 GROUP BY id_bibrec) as grp on cmt.id_bibrec = grp.id_bibrec WHERE id_user=%s AND star_score = 0 ORDER BY grp.maxdatecreation DESC, cmt.date_creation DESC" elif selected_order_by_option == "grof": - query = "SELECT cmt.id_bibrec, cmt.id, cmt.date_creation, cmt.body, cmt.status, cmt.in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT as cmt left join (SELECT min(date_creation) as mindatecreation, id_bibrec FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0 GROUP BY id_bibrec) as grp on cmt.id_bibrec = grp.id_bibrec WHERE id_user=%s AND star_score = 0 ORDER BY grp.mindatecreation ASC" + query = "SELECT cmt.id_bibrec, cmt.id, cmt.date_creation, cmt.body, cmt.body_format, cmt.status, cmt.in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT as cmt left join (SELECT min(date_creation) as mindatecreation, id_bibrec FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0 GROUP BY id_bibrec) as grp on cmt.id_bibrec = grp.id_bibrec WHERE id_user=%s AND star_score = 0 ORDER BY grp.mindatecreation ASC" if selected_display_number_option.isdigit(): selected_display_number_option_as_int = int(selected_display_number_option) @@ -2180,7 +2360,7 @@ def perform_display_your_comments(user_info, if selected_order_by_option in ("grlf", "grof"): res = run_sql(query + query_params, (user_info['uid'], user_info['uid'])) else: - res = run_sql("SELECT id_bibrec, id, date_creation, body, status, in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0" + query_params, (user_info['uid'], )) + res = run_sql("SELECT id_bibrec, id, date_creation, body, body_format, status, in_reply_to_id_cmtRECORDCOMMENT FROM cmtRECORDCOMMENT WHERE id_user=%s AND star_score = 0" + query_params, (user_info['uid'], )) return webcomment_templates.tmpl_your_comments(user_info, res, page_number=page_number, diff --git a/modules/webcomment/lib/webcomment_config.py b/modules/webcomment/lib/webcomment_config.py index 485b9f7dbd..ba7ffac9cf 100644 --- a/modules/webcomment/lib/webcomment_config.py +++ b/modules/webcomment/lib/webcomment_config.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# + # This file is part of Invenio. # Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. # @@ -30,6 +30,24 @@ 'REPORT_ABUSE': 'A' } +CFG_WEBCOMMENT_BODY_FORMATS = { + "HTML" : "HTML", + "TEXT" : "TXT", + "MARKDOWN" : "MD", +} + +CFG_WEBCOMMENT_OUTPUT_FORMATS = { + "HTML" : { + "WEB" : "WEB", + "EMAIL" : "HTML_EMAIL", + "CKEDITOR" : "CKEDITOR", + }, + "TEXT" : { + "EMAIL" : "TEXT_EMAIL", + "TEXTAREA" : "TEXTAREA", + }, +} + # Exceptions: errors class InvenioWebCommentError(Exception): """A generic error for WebComment.""" diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index d47089e80c..c1e308d1a6 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# Comments and reviews for records. # This file is part of Invenio. # Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 CERN. @@ -23,28 +22,40 @@ __revision__ = "$Id$" import cgi +import re +import html2text +import markdown2 # Invenio imports +from invenio.webcomment_config import \ + CFG_WEBCOMMENT_BODY_FORMATS, \ + CFG_WEBCOMMENT_OUTPUT_FORMATS from invenio.urlutils import create_html_link, create_url -from invenio.webuser import get_user_info, collect_user_info, isGuestUser, get_email +from invenio.webuser import get_user_info, \ + collect_user_info, \ + isGuestUser, \ + get_email from invenio.dateutils import convert_datetext_to_dategui from invenio.webmessage_mailutils import email_quoted_txt2html -from invenio.config import CFG_SITE_URL, \ - CFG_SITE_SECURE_URL, \ - CFG_BASE_URL, \ - CFG_SITE_LANG, \ - CFG_SITE_NAME, \ - CFG_SITE_NAME_INTL,\ - CFG_SITE_SUPPORT_EMAIL,\ - CFG_WEBCOMMENT_ALLOW_REVIEWS, \ - CFG_WEBCOMMENT_ALLOW_COMMENTS, \ - CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, \ - CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN, \ - CFG_WEBCOMMENT_AUTHOR_DELETE_COMMENT_OPTION, \ - CFG_CERN_SITE, \ - CFG_SITE_RECORD, \ - CFG_WEBCOMMENT_MAX_ATTACHED_FILES, \ - CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE +from invenio.config import \ + CFG_SITE_URL, \ + CFG_SITE_SECURE_URL, \ + CFG_BASE_URL, \ + CFG_SITE_LANG, \ + CFG_SITE_NAME, \ + CFG_SITE_NAME_INTL,\ + CFG_SITE_SUPPORT_EMAIL,\ + CFG_WEBCOMMENT_ALLOW_REVIEWS, \ + CFG_WEBCOMMENT_ALLOW_COMMENTS, \ + CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, \ + CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN, \ + CFG_WEBCOMMENT_AUTHOR_DELETE_COMMENT_OPTION, \ + CFG_CERN_SITE, \ + CFG_SITE_RECORD, \ + CFG_WEBCOMMENT_MAX_ATTACHED_FILES, \ + CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE, \ + CFG_WEBCOMMENT_ENABLE_HTML_EMAILS, \ + CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING from invenio.htmlutils import get_html_text_editor, create_html_select from invenio.messages import gettext_set_language from invenio.bibformat import format_record @@ -76,6 +87,7 @@ def tmpl_get_first_comments_without_ranking(self, recID, ln, comments, nb_commen c_status = 4 c_nb_reports = 5 c_id = 6 + c_body_format = 10 warnings = self.tmpl_warnings(warnings, ln) @@ -119,6 +131,7 @@ def tmpl_get_first_comments_without_ranking(self, recID, ln, comments, nb_commen comment_uid=comment[c_user_id], date_creation=comment[c_date_creation], body=comment[c_body], + body_format=comment[c_body_format], status=comment[c_status], nb_reports=comment[c_nb_reports], reply_link=reply_link, @@ -232,6 +245,7 @@ def tmpl_get_first_comments_with_ranking(self, recID, ln, comments=None, nb_comm c_star_score = 8 c_title = 9 c_id = 10 + c_body_format = 14 warnings = self.tmpl_warnings(warnings, ln) @@ -282,6 +296,7 @@ def tmpl_get_first_comments_with_ranking(self, recID, ln, comments=None, nb_comm comment_uid=comment[c_user_id], date_creation=comment[c_date_creation], body=comment[c_body], + body_format=comment[c_body_format], status=comment[c_status], nb_reports=comment[c_nb_reports], nb_votes_total=comment[c_nb_votes_total], @@ -362,7 +377,7 @@ def tmpl_get_first_comments_with_ranking(self, recID, ln, comments=None, nb_comm write_button_form) return out - def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_creation, body, status, nb_reports, reply_link=None, report_link=None, undelete_link=None, delete_links=None, unreport_link=None, recID=-1, com_id='', attached_files=None, collapsed_p=False, admin_p=False): + def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_creation, body, body_format, status, nb_reports, reply_link=None, report_link=None, undelete_link=None, delete_links=None, unreport_link=None, recID=-1, com_id='', attached_files=None, collapsed_p=False, admin_p=False): """ private function @param req: request object to fetch user info @@ -394,7 +409,13 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ if attached_files is None: attached_files = [] out = '' - final_body = email_quoted_txt2html(body) + + final_body = self.tmpl_prepare_comment_body( + body, + body_format, + CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"] + ) + title = nickname title += '' % (com_id, com_id) links = '' @@ -476,9 +497,9 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ ¶
    -
    - %(body)s -
    +
    + %(body)s +
    %(attached_files_html)s
    %(links)s
    @@ -499,7 +520,7 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ } return out - def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_creation, body, status, nb_reports, nb_votes_total, nb_votes_yes, star_score, title, report_link=None, delete_links=None, undelete_link=None, unreport_link=None, recID=-1, admin_p=False): + def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_creation, body, body_format, status, nb_reports, nb_votes_total, nb_votes_yes, star_score, title, report_link=None, delete_links=None, undelete_link=None, unreport_link=None, recID=-1, admin_p=False): """ private function @param req: request object to fetch user info @@ -531,6 +552,12 @@ def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_cre out = "" + final_body = self.tmpl_prepare_comment_body( + body, + body_format, + CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"] + ) + date_creation = convert_datetext_to_dategui(date_creation, ln=ln) reviewed_label = _("Reviewed by %(x_nickname)s on %(x_date)s") % {'x_nickname': nickname, 'x_date':date_creation} ## FIX @@ -541,10 +568,10 @@ def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_cre links = '' _body = '' if body != '': - _body = ''' -
    -%s -
    ''' % email_quoted_txt2html(body, linebreak_html='') + _body = ''' +
    + %s +
    ''' % final_body # Check if user is a comment moderator record_primary_collection = guess_primary_collection_of_a_record(recID) @@ -678,7 +705,8 @@ def tmpl_get_comments(self, req, recID, ln, c_round_name = 11 c_restriction = 12 reply_to = 13 - c_visibility = 14 + c_body_format = 14 + c_visibility = 15 discussion = 'reviews' comments_link = '%s (%i)' % (CFG_SITE_URL, CFG_SITE_RECORD, recID, _('Comments'), total_nb_comments) reviews_link = '%s (%i)' % (_('Reviews'), total_nb_reviews) @@ -694,7 +722,8 @@ def tmpl_get_comments(self, req, recID, ln, c_round_name = 7 c_restriction = 8 reply_to = 9 - c_visibility = 10 + c_body_format = 10 + c_visibility = 11 discussion = 'comments' comments_link = '%s (%i)' % (_('Comments'), total_nb_comments) reviews_link = '%s (%i)' % (CFG_SITE_URL, CFG_SITE_RECORD, recID, _('Reviews'), total_nb_reviews) @@ -851,14 +880,14 @@ def tmpl_get_comments(self, req, recID, ln, delete_links['auth'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_auth?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) undelete_link = "%s/admin/webcomment/webcommentadmin.py/undel_com?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) unreport_link = "%s/admin/webcomment/webcommentadmin.py/unreport_com?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) - comments_rows += self.tmpl_get_comment_without_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_status], comment[c_nb_reports], reply_link, report_link, undelete_link, delete_links, unreport_link, recID, comment[c_id], files, comment[c_visibility]) + comments_rows += self.tmpl_get_comment_without_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_body_format], comment[c_status], comment[c_nb_reports], reply_link, report_link, undelete_link, delete_links, unreport_link, recID, comment[c_id], files, comment[c_visibility]) else: report_link = '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/reviews/report?ln=%(ln)s&comid=%%(comid)s&do=%(do)s&ds=%(ds)s&nb=%(nb)s&p=%(p)s&referer=%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/reviews/display' % useful_dict % {'comid': comment[c_id]} delete_links['mod'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_mod?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) delete_links['auth'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_auth?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) undelete_link = "%s/admin/webcomment/webcommentadmin.py/undel_com?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) unreport_link = "%s/admin/webcomment/webcommentadmin.py/unreport_com?ln=%s&id=%s" % (CFG_SITE_URL, ln, comment[c_id]) - comments_rows += self.tmpl_get_comment_with_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_status], comment[c_nb_reports], comment[c_nb_votes_total], comment[c_nb_votes_yes], comment[c_star_score], comment[c_title], report_link, delete_links, undelete_link, unreport_link, recID) + comments_rows += self.tmpl_get_comment_with_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_body_format], comment[c_status], comment[c_nb_reports], comment[c_nb_votes_total], comment[c_nb_votes_yes], comment[c_star_score], comment[c_title], report_link, delete_links, undelete_link, unreport_link, recID) helpful_label = _("Was this review helpful?") report_abuse_label = "(" + _("Report abuse") + ")" yes_no_separator = '
    /
    @@ -1974,6 +2033,7 @@ def tmpl_admin_comments(self, ln, uid, comID, recID, comment_data, reviews, erro cmt_tuple[1],#userid cmt_tuple[2],#date_creation cmt_tuple[3],#body + cmt_tuple[10],#body_format cmt_tuple[9],#status 0, cmt_tuple[5],#nb_votes_total @@ -1988,6 +2048,7 @@ def tmpl_admin_comments(self, ln, uid, comID, recID, comment_data, reviews, erro cmt_tuple[1],#userid cmt_tuple[2],#date_creation cmt_tuple[3],#body + cmt_tuple[6],#body_format cmt_tuple[5],#status 0, None, #reply_link @@ -2232,10 +2293,17 @@ def tmpl_mini_review(self, recID, ln=CFG_SITE_LANG, action='SUBMIT', } return out - def tmpl_email_new_comment_header(self, recID, title, reviews, - comID, report_numbers, - can_unsubscribe=True, - ln=CFG_SITE_LANG, uid=-1): + def tmpl_email_new_comment_header( + self, + recID, + title, + reviews, + comID, + report_numbers, + can_unsubscribe=True, + ln=CFG_SITE_LANG, + uid=-1, + html_p=False): """ Prints the email header used to notify subscribers that a new comment/review was added. @@ -2258,14 +2326,28 @@ def tmpl_email_new_comment_header(self, recID, title, reviews, _("The following comment was sent to %(CFG_SITE_NAME)s by %(user_nickname)s:")) % \ {'CFG_SITE_NAME': CFG_SITE_NAME, 'user_nickname': user_info['nickname']} - out += '\n(<%s>)' % (CFG_SITE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID)) + if html_p: + out += '
    (<
    %s>)' % (CFG_SITE_SECURE_URL + '/' + CFG_SITE_RECORD + '/' + str(recID), CFG_SITE_URL + '/' + CFG_SITE_RECORD + '/' + str(recID)) + else: + out += '\n(<%s>)' % (CFG_SITE_SECURE_URL + '/' + CFG_SITE_RECORD + '/' + str(recID)) + out += '\n\n\n' + + if html_p: + out = out.replace('\n','
    ') + return out - def tmpl_email_new_comment_footer(self, recID, title, reviews, - comID, report_numbers, - can_unsubscribe=True, - ln=CFG_SITE_LANG): + def tmpl_email_new_comment_footer( + self, + recID, + title, + reviews, + comID, + report_numbers, + can_unsubscribe=True, + ln=CFG_SITE_LANG, + html_p=False): """ Prints the email footer used to notify subscribers that a new comment/review was added. @@ -2278,32 +2360,59 @@ def tmpl_email_new_comment_footer(self, recID, title, reviews, @param can_unsubscribe: True if user can unsubscribe from alert @param ln: language """ + # load the right message language _ = gettext_set_language(ln) out = '\n\n-- \n' out += _("This is an automatic message, please don't reply to it.") out += '\n' - out += _("To post another comment, go to <%(x_url)s> instead.") % \ - {'x_url': CFG_SITE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + + if html_p: + out += _("To post another comment, go to <%(x_url)s> instead.") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ (reviews and '/reviews' or '/comments') + '/add'} - out += '\n' + out += '
    ' + else: + out += _("To post another comment, go to <%(x_url)s> instead.") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + (reviews and '/reviews' or '/comments') + '/add'} + out += '\n' + if not reviews: - out += _("To specifically reply to this comment, go to <%(x_url)s>") % \ - {'x_url': CFG_SITE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + if html_p: + out += _("To specifically reply to this comment, go to <%(x_url)s>") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ '/comments/add?action=REPLY&comid=' + str(comID)} - out += '\n' + out += '
    ' + else: + out += _("To specifically reply to this comment, go to <%(x_url)s>") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + '/comments/add?action=REPLY&comid=' + str(comID)} + out += '\n' + if can_unsubscribe: - out += _("To unsubscribe from this discussion, go to <%(x_url)s>") % \ - {'x_url': CFG_SITE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + if html_p: + out += _("To unsubscribe from this discussion, go to <%(x_url)s>") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ '/comments/unsubscribe'} - out += '\n' - out += _("For any question, please use <%(CFG_SITE_SUPPORT_EMAIL)s>") % \ + out += '
    ' + out += _("For any question, please use <%(CFG_SITE_SUPPORT_EMAIL)s>") % \ + {'CFG_SITE_SUPPORT_EMAIL': CFG_SITE_SUPPORT_EMAIL} + else: + out += _("To unsubscribe from this discussion, go to <%(x_url)s>") % \ + {'x_url': CFG_SITE_SECURE_URL + '/'+ CFG_SITE_RECORD +'/' + str(recID) + \ + '/comments/unsubscribe'} + out += '\n' + out += _("For any question, please use <%(CFG_SITE_SUPPORT_EMAIL)s>") % \ {'CFG_SITE_SUPPORT_EMAIL': CFG_SITE_SUPPORT_EMAIL} + if html_p: + out = out.replace('\n','
    ') + return out - def tmpl_email_new_comment_admin(self, recID): + def tmpl_email_new_comment_admin(self, recID, html_p=False): """ Prints the record information used in the email to notify the system administrator that a new comment has been posted. @@ -2324,11 +2433,23 @@ def tmpl_email_new_comment_admin(self, recID): report_nums = ', '.join(report_nums) #for rep_num in report_nums: # res_rep_num = res_rep_num + ', ' + rep_num - out += " Title = %s \n" % (title and title[0] or "No Title") - out += " Authors = %s \n" % authors + if html_p: + out += "
  • Title = %s
  • " % (title and title[0] or "No Title") + else: + out += " Title = %s \n" % (title and title[0] or "No Title") + if html_p: + out += "
  • Authors = %s
  • " % authors + else: + out += " Authors = %s \n" % authors if dates: - out += " Date = %s \n" % dates[0] - out += " Report number = %s" % report_nums + if html_p: + out += "
  • Date = %s
  • " % dates[0] + else: + out += " Date = %s \n" % dates[0] + if html_p: + out += "
  • Report number = %s
  • " % report_nums + else: + out += " Report number = %s" % report_nums return out @@ -2496,7 +2617,7 @@ def tmpl_your_comments(self, user_info, comments, page_number=1, selected_order_ last_id_bibrec = None nb_record_groups = 0 out += '
    ' - for id_bibrec, comid, date_creation, body, status, in_reply_to_id_cmtRECORDCOMMENT in comments: + for id_bibrec, comid, date_creation, body, body_format, status, in_reply_to_id_cmtRECORDCOMMENT in comments: if last_id_bibrec != id_bibrec and selected_display_format_option in ('rc', 'ro'): # We moved to another record. Show some info about # current record. @@ -2515,7 +2636,13 @@ def tmpl_your_comments(self, user_info, comments, page_number=1, selected_order_
    • ''' % {'recid': id_bibrec} + \ record_info_html + '
    ' if selected_display_format_option != 'ro': - final_body = email_quoted_txt2html(body) + + final_body = self.tmpl_prepare_comment_body( + body, + body_format, + CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"] + ) + title = '' % (comid, comid) if status == "dm": final_body = '
    %s
    ' % _("Comment deleted by the moderator") @@ -2544,9 +2671,9 @@ def tmpl_your_comments(self, user_info, comments, page_number=1, selected_order_ ¶
    -
    - %(body)s -
    +
    + %(body)s +
    %(links)s
    @@ -2612,3 +2739,181 @@ def tmpl_your_comments(self, user_info, comments, page_number=1, selected_order_ out += '
    ' return out + + def tmpl_prepare_comment_body(self, body, body_format, output_format): + """ + Prepares the comment's body according to the desired format + """ + + if body_format == CFG_WEBCOMMENT_BODY_FORMATS["HTML"]: + + if output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"]: + # Display stuff as it is. It should already be washed. + pass + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"]: + # Prefer inline styles to support most e-mail clients. + body = body.replace( + "
    ", + "
    " + ) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["CKEDITOR"]: + # CKEditor needs to have stuff unescaped once more + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + elif output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["TEXTAREA"]: + # Convert HTML to plain text + body = html2text.html2text(body) + # html2text appends 2 unnecessary newlines at the end of the body + if body[-2:] == "\n\n": + body = body[:-2] + # The following doubles ">" at the beginning of each line: + # "> test" --> ">> test", ">>> test" --> ">>>>>> test" + # We currently use single ">" to quote text, so no need + # to use it for now. + #body = re.sub(r"^(>+)", r"\1" * 2, body, count=0, flags=re.M) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"]: + # Convert HTML to plain text + body = html2text.html2text(body) + # html2text appends 2 unnecessary newlines at the end of the body + if body[-2:] == "\n\n": + body = body[:-2] + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + elif body_format == CFG_WEBCOMMENT_BODY_FORMATS["TEXT"]: + + if output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"]: + # Convert plain text to HTML + body = email_quoted_txt2html( + body, + indent_txt=">", + indent_html=("
    ", "
    "), + wash_p=False + ) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"]: + # Convert plain text to HTML + # Prefer inline styles to support most e-mail clients. + body = email_quoted_txt2html( + body, + indent_txt=">", + indent_html=( + "
    ", + "
    " + ), + wash_p=False + ) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["CKEDITOR"]: + # Convert plain text to HTML + body = email_quoted_txt2html( + body, + indent_txt=">", + indent_html=("
    ", "
    "), + wash_p=False + ) + # CKEditor needs to have stuff unescaped once more + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + elif output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["TEXTAREA"]: + # Display stuff as it is. It is escaped before being + # placed in the textarea anyway. + pass + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"]: + # Display stuff as it is. + pass + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + elif body_format == CFG_WEBCOMMENT_BODY_FORMATS["MARKDOWN"]: + + if output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["WEB"]: + # Convert Markdown to HTML + body = markdown2.markdown( + body, + safe_mode="escape" + ) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["EMAIL"]: + # Convert Markdown to HTML + body = markdown2.markdown( + body, + safe_mode="escape" + ) + # Prefer inline styles to support most e-mail clients. + body = body.replace( + "
    ", + "
    " + ) + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["HTML"]["CKEDITOR"]: + # Convert Markdown to HTML + body = markdown2.markdown( + body, + safe_mode="escape" + ) + # CKEditor needs to have stuff unescaped once more + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + elif output_format in CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"].values(): + + if output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["TEXTAREA"]: + # Display stuff as it is. It is escaped before being + # placed in the textarea anyway. + pass + + elif output_format == CFG_WEBCOMMENT_OUTPUT_FORMATS["TEXT"]["EMAIL"]: + # Display stuff as it is. + pass + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + else: + # Let's always be on the safe side + body = cgi.escape(body) + + return body diff --git a/modules/webcomment/lib/webcomment_unit_tests.py b/modules/webcomment/lib/webcomment_unit_tests.py index 2e081584b2..8548dc9ee8 100644 --- a/modules/webcomment/lib/webcomment_unit_tests.py +++ b/modules/webcomment/lib/webcomment_unit_tests.py @@ -44,7 +44,89 @@ def test_with_random_values(self): self.assert_(calculate_start_date('77d') > '2009-04-23 14:51:31') self.assert_(calculate_start_date('20d') > '2009-06-19 14:51:55') -TEST_SUITE = make_test_suite(TestCalculateStartDate) +class TestCommentFormats(InvenioTestCase): + + def test_conversions(self): + from invenio.webcomment_templates import Template + from invenio.webcomment_config import CFG_WEBCOMMENT_BODY_FORMATS, CFG_WEBCOMMENT_OUTPUT_FORMATS + t = Template() + + + test_body = """
    \n\n\n

    heading1\n\n\n

    \n\n\nstrong
    test</plaintext></div>""" + + ## BODY_FORMAT = HTML + + expected_output = """<div>\n\n\n<script>alert("xss");</script><h1>heading1\n\n\n</h1><div>\n\n\n<strong>strong</strong></div><plaintext>test</plaintext></div>""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['HTML'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['WEB']) + assert expected_output == output + + expected_output = """<div>\n\n\n<script>alert("xss");</script><h1>heading1\n\n\n</h1><div>\n\n\n<strong>strong</strong></div><plaintext>test</plaintext></div>""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['HTML'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['EMAIL']) + assert expected_output == output + + expected_output = """&lt;div&gt;\n\n\n&lt;script&gt;alert("xss");&lt;/script&gt;&lt;h1&gt;heading1\n\n\n&lt;/h1&gt;&lt;div&gt;\n\n\n&lt;strong&gt;strong&lt;/strong&gt;&lt;/div&gt;&lt;plaintext&gt;test&lt;/plaintext&gt;&lt;/div&gt;""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['HTML'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['CKEDITOR']) + assert expected_output == output + + expected_output = """# heading1\n\n**strong**\n\ntest""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['HTML'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['TEXTAREA']) + assert expected_output == output + + expected_output = """# heading1\n\n**strong**\n\ntest""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['HTML'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['EMAIL']) + assert expected_output == output + + + ## BODY_FORMAT = TEXT + test_body = """Hello, <div>\n\n\n<script>alert("xss");</script><h1>heading1\n\n\n</h1><div>\n\n\n<strong>strong</strong></div><plaintext>test</plaintext></div>\n\n testtest""" + + expected_output = """Hello, <div>\n\n\n<script>alert("xss");</script><h1>heading1\n\n\n</h1><div>\n\n\n<strong>strong</strong></div><plaintext>test</plaintext></div>\n\n testtest""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['TEXT'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['TEXTAREA']) + assert expected_output == output + + expected_output = """Hello, <div>\n\n\n<script>alert("xss");</script><h1>heading1\n\n\n</h1><div>\n\n\n<strong>strong</strong></div><plaintext>test</plaintext></div>\n\n testtest""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['TEXT'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['EMAIL']) + assert expected_output == output + + expected_output = """Hello, &lt;div&gt;<br/>\n<br/>\n<br/>\n&lt;script&gt;alert("xss");&lt;/script&gt;&lt;h1&gt;heading1<br/>\n<br/>\n<br/>\n&lt;/h1&gt;&lt;div&gt;<br/>\n<br/>\n<br/>\n&lt;strong&gt;strong&lt;/strong&gt;&lt;/div&gt;&lt;plaintext&gt;test&lt;/plaintext&gt;&lt;/div&gt;<br/>\n<br/>\n testtest<br/>\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['TEXT'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['WEB']) + assert expected_output == output + + expected_output = """Hello, &lt;div&gt;<br/>\n<br/>\n<br/>\n&lt;script&gt;alert("xss");&lt;/script&gt;&lt;h1&gt;heading1<br/>\n<br/>\n<br/>\n&lt;/h1&gt;&lt;div&gt;<br/>\n<br/>\n<br/>\n&lt;strong&gt;strong&lt;/strong&gt;&lt;/div&gt;&lt;plaintext&gt;test&lt;/plaintext&gt;&lt;/div&gt;<br/>\n<br/>\n testtest<br/>\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['TEXT'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['EMAIL']) + assert expected_output == output + + expected_output = """Hello, &amp;lt;div&amp;gt;&lt;br/&gt;\n&lt;br/&gt;\n&lt;br/&gt;\n&amp;lt;script&amp;gt;alert("xss");&amp;lt;/script&amp;gt;&amp;lt;h1&amp;gt;heading1&lt;br/&gt;\n&lt;br/&gt;\n&lt;br/&gt;\n&amp;lt;/h1&amp;gt;&amp;lt;div&amp;gt;&lt;br/&gt;\n&lt;br/&gt;\n&lt;br/&gt;\n&amp;lt;strong&amp;gt;strong&amp;lt;/strong&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;plaintext&amp;gt;test&amp;lt;/plaintext&amp;gt;&amp;lt;/div&amp;gt;&lt;br/&gt;\n&lt;br/&gt;\n testtest&lt;br/&gt;\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['TEXT'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['CKEDITOR']) + assert expected_output == output + + ## BODY_FORMAT = MARKDOWN + test_body = """An h2 header\n------------\n\nHere's a numbered list:\n\n 1. first item\n 2. second item\n 3. third item""" + + expected_output = """<h2>An h2 header</h2>\n\n<p>Here's a numbered list:</p>\n\n<ol>\n<li>first item</li>\n<li>second item</li>\n<li>third item</li>\n</ol>\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['MARKDOWN'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['WEB']) + assert expected_output == output + + expected_output = """<h2>An h2 header</h2>\n\n<p>Here's a numbered list:</p>\n\n<ol>\n<li>first item</li>\n<li>second item</li>\n<li>third item</li>\n</ol>\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['MARKDOWN'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['EMAIL']) + assert expected_output == output + + expected_output = """&lt;h2&gt;An h2 header&lt;/h2&gt;\n\n&lt;p&gt;Here\'s a numbered list:&lt;/p&gt;\n\n&lt;ol&gt;\n&lt;li&gt;first item&lt;/li&gt;\n&lt;li&gt;second item&lt;/li&gt;\n&lt;li&gt;third item&lt;/li&gt;\n&lt;/ol&gt;\n""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['MARKDOWN'], CFG_WEBCOMMENT_OUTPUT_FORMATS['HTML']['CKEDITOR']) + assert expected_output == output + + expected_output ="""An h2 header\n------------\n\nHere's a numbered list:\n\n 1. first item\n 2. second item\n 3. third item""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['MARKDOWN'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['TEXTAREA']) + assert expected_output == output + + expected_output ="""An h2 header\n------------\n\nHere's a numbered list:\n\n 1. first item\n 2. second item\n 3. third item""" + output = t.tmpl_prepare_comment_body(test_body, CFG_WEBCOMMENT_BODY_FORMATS['MARKDOWN'], CFG_WEBCOMMENT_OUTPUT_FORMATS['TEXT']['EMAIL']) + assert expected_output == output + + return output + + +TEST_SUITE = make_test_suite(TestCalculateStartDate, TestCommentFormats) if __name__ == "__main__": run_test_suite(TEST_SUITE) diff --git a/modules/webcomment/lib/webcommentadminlib.py b/modules/webcomment/lib/webcommentadminlib.py index fac14b4b6c..b203a134a4 100644 --- a/modules/webcomment/lib/webcommentadminlib.py +++ b/modules/webcomment/lib/webcommentadminlib.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# + # This file is part of Invenio. # Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 CERN. # @@ -131,7 +131,7 @@ def perform_request_delete(comID=-1, recID=-1, uid=-1, reviews="", ln=CFG_SITE_L if comment: # Figure out if this is a review or a comment - c_star_score = 5 + c_star_score = 6 if comment[c_star_score] > 0: reviews = 1 else: @@ -359,11 +359,12 @@ def query_get_comments(uid, cmtID, recID, reviews, ln, abuse=False, user_collect tuple (nickname, uid, date_creation, body, nb_votes_yes, nb_votes_total, star_score, title, id, status) """ qdict = {'id': 0, 'id_bibrec': 1, 'uid': 2, 'date_creation': 3, 'body': 4, - 'status': 5, 'nb_abuse_reports': 6, 'nb_votes_yes': 7, 'nb_votes_total': 8, - 'star_score': 9, 'title': 10, 'email': -2, 'nickname': -1} + 'status': 5, 'nb_abuse_reports': 6, 'body_format': 7, 'nb_votes_yes': 8, 'nb_votes_total': 9, + 'star_score': 10, 'title': 11, 'email': -2, 'nickname': -1} query = """SELECT c.id, c.id_bibrec, c.id_user, DATE_FORMAT(c.date_creation, '%%Y-%%m-%%d %%H:%%i:%%S'), c.body, c.status, c.nb_abuse_reports, + c.body_format, %s u.email, u.nickname FROM cmtRECORDCOMMENT c LEFT JOIN user u @@ -403,14 +404,16 @@ def query_get_comments(uid, cmtID, recID, reviews, ln, abuse=False, user_collect qtuple[qdict['star_score']], qtuple[qdict['title']], qtuple[qdict['id']], - qtuple[qdict['status']]) + qtuple[qdict['status']], + qtuple[qdict['body_format']]) else: comment_tuple = (nickname, qtuple[qdict['uid']], qtuple[qdict['date_creation']], qtuple[qdict['body']], qtuple[qdict['id']], - qtuple[qdict['status']]) + qtuple[qdict['status']], + qtuple[qdict['body_format']]) general_infos_tuple = (nickname, qtuple[qdict['uid']], qtuple[qdict['email']], diff --git a/modules/webmessage/lib/webmessage_mailutils.py b/modules/webmessage/lib/webmessage_mailutils.py index 6e6ca0a04a..c7651114bf 100644 --- a/modules/webmessage/lib/webmessage_mailutils.py +++ b/modules/webmessage/lib/webmessage_mailutils.py @@ -31,7 +31,8 @@ def email_quoted_txt2html(text, linebreak_txt="\n", indent_html=('<div class="commentbox">', "</div>"), linebreak_html='<br/>', - indent_block=True): + indent_block=True, + wash_p=True): """ Takes a typical mail quoted text, e.g.:: hello, @@ -42,7 +43,7 @@ def email_quoted_txt2html(text, >> No. Now, go away, or I shall taunt you a second time-a! I think we're not going to be friends! - and return an html formatted output, e.g.:: + and returns HTML formatted output, e.g.:: hello,<br/> you told me:<br/> <div> @@ -89,15 +90,27 @@ def email_quoted_txt2html(text, @param indent_block: if indentation should be done per 'block' i.e. only at changes of indentation level (+1, -1) or at each line. + @param wash_p: if each line should be washed or simply escaped. @return: string containing html formatted output """ - washer = HTMLWasher() - final_body = "" + # If needed, instantiate the HTMLWasher for later + if wash_p: + washer = HTMLWasher() + + # Some initial values + out = "" nb_indent = 0 + (indent_html_open, indent_html_close) = indent_html + + # Clean off any newlines from around the input text = text.strip('\n') + + # Iterate over the lines in our input lines = text.split(linebreak_txt) for line in lines: + + # Calculate how indented this line is new_nb_indent = 0 while True: if line.startswith(indent_txt): @@ -105,42 +118,73 @@ def email_quoted_txt2html(text, line = line[len(indent_txt):] else: break + + # In this case we are indenting the entire block if indent_block: + # This line is more indented than the previous one, + # therefore, open some indentation. if (new_nb_indent > nb_indent): for dummy in range(nb_indent, new_nb_indent): - final_body += tabs_before*"\t" + indent_html[0] + "\n" + out += tabs_before*"\t" + indent_html_open + "\n" tabs_before += 1 + # This line is less indented than the previous one, + # therefore, close some indentation. elif (new_nb_indent < nb_indent): for dummy in range(new_nb_indent, nb_indent): tabs_before -= 1 - final_body += (tabs_before)*"\t" + indent_html[1] + "\n" + out += (tabs_before)*"\t" + indent_html_close + "\n" + # This line is as indented as the previous one, + # therefore, only add the needed tabs. else: - final_body += (tabs_before)*"\t" + out += (tabs_before)*"\t" + # And in this case we are indenting each line separately + else: + out += tabs_before*"\t" + new_nb_indent * indent_html_open + + # We can wash this line... + if wash_p: + try: + line = washer.wash(line) + except HTMLParseError: + # Line contained something like "foo<bar" + line = cgi.escape(line) + # ...or simply escape it as it is. else: - final_body += tabs_before*"\t" + new_nb_indent * indent_html[0] - try: - line = washer.wash(line) - except HTMLParseError: - # Line contained something like "foo<bar" line = cgi.escape(line) + + # Add the needed tabs for the nicer visual formatting if indent_block: - final_body += tabs_before*"\t" - final_body += line + out += tabs_before*"\t" + + # Add the current line to the output + out += line + + # In case we are indenting each line separately, + # close all previously opened indentation. if not indent_block: - final_body += new_nb_indent * indent_html[1] - final_body += linebreak_html + "\n" + out += new_nb_indent * indent_html_close + + # Add the line break to the output after each line + out += linebreak_html + "\n" + + # Reset the current line's indentation level nb_indent = new_nb_indent + + # In case we are indenting the entire block, + # close all previously opened indentation. if indent_block: for dummy in range(0, nb_indent): tabs_before -= 1 - final_body += (tabs_before)*"\t" + "</div>\n" - return final_body + out += (tabs_before)*"\t" + indent_html_close + "\n" + # Return the output + return out def email_quote_txt(text, indent_txt='>>', linebreak_input="\n", - linebreak_output="\n"): + linebreak_output="\n", + escape_p=False): """ Takes a text and returns it in a typical mail quoted format, e.g.:: C'est un lapin, lapin de bois. @@ -162,17 +206,29 @@ def email_quote_txt(text, @param indent_txt: the string used for quoting (default: '>>') @param linebreak_input: in the text param, string used for linebreaks @param linebreak_output: linebreak used for output + @param escape_p: if True, escape the text before returning it @return: the text as a quoted string """ - if (text == ""): - return "" - lines = text.split(linebreak_input) - text = "" - for line in lines: - text += indent_txt + line + linebreak_output - return text -def escape_email_quoted_text(text, indent_txt='>>', linebreak_txt='\n'): + out= "" + + if text: + + lines = text.split(linebreak_input) + + for line in lines: + out += indent_txt + line + linebreak_output + + if escape_p: + out = cgi.escape(out) + + return out + +def escape_email_quoted_text( + text, + indent_txt='>>', + linebreak_txt='\n', + wash_p=True): """ Escape text using an email-like indenting rule. As an example, this text:: @@ -194,8 +250,12 @@ def escape_email_quoted_text(text, indent_txt='>>', linebreak_txt='\n'): @param text: the string to escape @param indent_txt: the string used for quoting @param linebreak_txt: in the text param, string used for linebreaks + @param wash_p: if each line should be washed or simply escaped. """ - washer = HTMLWasher() + + if wash_p: + washer = HTMLWasher() + lines = text.split(linebreak_txt) output = '' for line in lines: @@ -207,6 +267,10 @@ def escape_email_quoted_text(text, indent_txt='>>', linebreak_txt='\n'): line = line[len(indent_txt):] else: break - output += (nb_indent * indent_txt) + washer.wash(line, render_unallowed_tags=True) + linebreak_txt + if wash_p: + output += (nb_indent * indent_txt) + washer.wash(line, render_unallowed_tags=True) + linebreak_txt + else: + output += (nb_indent * indent_txt) + cgi.escape(line) + linebreak_txt + nb_indent = 0 return output[:-1] diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index 288e75eaa2..b089862b59 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -3514,6 +3514,12 @@ a#advbox-toggle{ } .webcomment_comment_box blockquote { + padding: 0px 10px 0px 10px; + margin: 0px 0px 0px 10px; + border-left: 2px solid #3366CC; +} + +.webcomment_comment_body { margin: 10px; } diff --git a/modules/webstyle/etc/invenio-ckeditor-config.js b/modules/webstyle/etc/invenio-ckeditor-config.js index 59bf55ac55..2f33611c79 100644 --- a/modules/webstyle/etc/invenio-ckeditor-config.js +++ b/modules/webstyle/etc/invenio-ckeditor-config.js @@ -1,3 +1,24 @@ +/* +* -*- mode: text; coding: utf-8; -*- + + This file is part of Invenio. + Copyright (C) 2011, 2012, 2013, 2014 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. +*/ + /*Define here the config of the CKEditor used in Invenio. Users/admin: @@ -38,13 +59,7 @@ config.removePlugins = 'elementspath'; simulate the the ".commentbox" CSS class in WebComment case. */ config.contentsCss = ['/img/invenio.css', '/ckeditor/invenio-ckeditor-content.css']; -/* Though not recommended, it is much better that users gets a - <br/> when pressing carriage return than a <p> element. Then - when a user replies to a webcomment without the CKeditor, - line breaks are nicely displayed. -*/ -config.enterMode = CKEDITOR.ENTER_BR; - /* Load our Scientific Characters panel */ config.extraPlugins = 'scientificchar'; -} \ No newline at end of file + +} diff --git a/modules/webstyle/etc/invenio-ckeditor-content.css b/modules/webstyle/etc/invenio-ckeditor-content.css index 152fc3898c..a6ee4783fb 100644 --- a/modules/webstyle/etc/invenio-ckeditor-content.css +++ b/modules/webstyle/etc/invenio-ckeditor-content.css @@ -1,2 +1,26 @@ -blockquote {margin:0;padding:0;display:inline;} -blockquote div, blockquote p{padding: 0 10px 0px 10px;border-left: 2px solid #36c;margin-left: 10px;display:inline;} +/* +* -*- mode: text; coding: utf-8; -*- + + This file is part of Invenio. + Copyright (C) 2011, 2012, 2013, 2014 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. +*/ + +blockquote { + padding: 0px 10px 0px 10px; + margin: 0px 0px 0px 10px; + border-left: 2px solid #3366CC; +} diff --git a/requirements.txt b/requirements.txt index 6f17065ccc..606aeaefe2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,6 @@ redis==2.9.0 nydus==0.10.6 Cerberus==0.5 matplotlib +html2text==2014.7.3 +markdown2==2.2.1 +bleach==1.4 From a90617855b10b73b245af804e1c587de20795fef Mon Sep 17 00:00:00 2001 From: Avraam Tsantekidis <avraam.tsantekidis@cern.ch> Date: Fri, 15 Aug 2014 14:54:46 +0200 Subject: [PATCH 42/83] WebComment: bibdoc relations & filtering * FEATURE Creates database table to store the comment-bibdoc relations and provides functions to get/set the relations. * FEATURE Amends the interface so that users can: (a) relate comments to bibdocs when adding new comments, (b) filter the displayed comments based on those relations and (c) filter the displayed comments based on free text. Co-Authored-By: Harris Tzovanakis <me@drjova.com> Co-Authored-By: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> Co-Authored-By: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Signed-off-by: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> --- Makefile.am | 23 + ..._08_19_comment_to_bibdoc_relation_table.py | 47 ++ modules/miscutil/sql/tabcreate.sql | 10 + modules/miscutil/sql/tabdrop.sql | 1 + modules/webcomment/lib/Makefile.am | 3 +- modules/webcomment/lib/webcomment.py | 238 ++++++-- modules/webcomment/lib/webcomment_dblayer.py | 86 +++ .../webcomment/lib/webcomment_templates.py | 566 +++++++++++++++++- .../webcomment/lib/webcomment_webinterface.py | 77 ++- modules/webstyle/css/invenio.css | 100 +++- modules/webstyle/img/Makefile.am | 3 +- modules/webstyle/img/filter_filled.png | Bin 0 -> 382 bytes 12 files changed, 1043 insertions(+), 111 deletions(-) create mode 100644 modules/miscutil/lib/upgrades/invenio_2014_08_19_comment_to_bibdoc_relation_table.py create mode 100644 modules/webcomment/lib/webcomment_dblayer.py create mode 100644 modules/webstyle/img/filter_filled.png diff --git a/Makefile.am b/Makefile.am index 6f44617c95..5df1a7df68 100644 --- a/Makefile.am +++ b/Makefile.am @@ -324,6 +324,29 @@ uninstall-pdfa-helper-files: @echo "** The PDF/A helper files were successfully uninstalled. **" @echo "***********************************************************" +install-webcomment: + @echo "***********************************************************" + @echo "** Installing Webcomment plugin dependencies. **" + @echo "***********************************************************" + rm -rf /tmp/webcomment + mkdir /tmp/webcomment + wget 'https://github.com/cowboy/jquery-throttle-debounce/archive/master.zip' -O '/tmp/webcomment/webcomment.zip' --no-check-certificate + unzip -u -d '/tmp/webcomment' '/tmp/webcomment/webcomment.zip' + mv /tmp/webcomment/jquery-throttle-debounce-master/jquery.ba-throttle-debounce.min.js ${prefix}/var/www/js + wget 'https://raw.githubusercontent.com/bartaz/sandbox.js/master/jquery.highlight.js' -O '/tmp/webcomment/jquery.highlight.min.js' --no-check-certificate + mv /tmp/webcomment/jquery.highlight.min.js ${prefix}/var/www/js + rm -rf /tmp/webcomment + @echo "***********************************************************" + @echo "** Webcomment plugins were successfully installed. **" + @echo "***********************************************************" + +uninstall-webcomment: + rm -f ${prefix}/var/www/js/jquery.ba-throttle-debounce.min.js + rm -f ${prefix}/var/www/js/jquery.highlight.min.js + @echo "***********************************************************" + @echo "** The Webcomment plugins were successfully uninstalled. **" + @echo "***********************************************************" + #Solrutils allows automatic installation, running and searching of an external Solr index. install-solrutils: @echo "***********************************************************" diff --git a/modules/miscutil/lib/upgrades/invenio_2014_08_19_comment_to_bibdoc_relation_table.py b/modules/miscutil/lib/upgrades/invenio_2014_08_19_comment_to_bibdoc_relation_table.py new file mode 100644 index 0000000000..f0ac2874d9 --- /dev/null +++ b/modules/miscutil/lib/upgrades/invenio_2014_08_19_comment_to_bibdoc_relation_table.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014 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(): + """Upgrader info.""" + return ("Create a new table to be used for relating comment ids and " + "their record ids with bibdoc ids") + + +def do_upgrade(): + """Perform upgrade.""" + run_sql(""" +CREATE TABLE IF NOT EXISTS `cmtRECORDCOMMENT_bibdoc` ( + `id_bibrec` mediumint(8) unsigned NOT NULL, + `id_cmtRECORDCOMMENT` int(15) unsigned NOT NULL, + `id_bibdoc` mediumint(9) unsigned NOT NULL, + `version` tinyint(4) unsigned NOT NULL, + PRIMARY KEY (`id_bibrec`,`id_cmtRECORDCOMMENT`), + KEY `id_cmtRECORDCOMMENT` (`id_cmtRECORDCOMMENT`), + KEY `id_bibdoc` (`id_bibdoc`) +) ENGINE=MyISAM;""") + + +def estimate(): + """ Estimate running time of upgrade in seconds (optional). """ + return 1 diff --git a/modules/miscutil/sql/tabcreate.sql b/modules/miscutil/sql/tabcreate.sql index 04af138884..516c4cfcfe 100644 --- a/modules/miscutil/sql/tabcreate.sql +++ b/modules/miscutil/sql/tabcreate.sql @@ -3916,6 +3916,16 @@ CREATE TABLE IF NOT EXISTS cmtCOLLAPSED ( PRIMARY KEY (id_user, id_bibrec, id_cmtRECORDCOMMENT) ) ENGINE=MyISAM; +CREATE TABLE IF NOT EXISTS `cmtRECORDCOMMENT_bibdoc` ( + `id_bibrec` mediumint(8) unsigned NOT NULL, + `id_cmtRECORDCOMMENT` int(15) unsigned NOT NULL, + `id_bibdoc` mediumint(9) unsigned NOT NULL, + `version` tinyint(4) unsigned NOT NULL, + PRIMARY KEY (`id_bibrec`,`id_cmtRECORDCOMMENT`), + KEY `id_cmtRECORDCOMMENT` (`id_cmtRECORDCOMMENT`), + KEY `id_bibdoc` (`id_bibdoc`) +) ENGINE=MyISAM; + -- tables for BibKnowledge: CREATE TABLE IF NOT EXISTS knwKB ( diff --git a/modules/miscutil/sql/tabdrop.sql b/modules/miscutil/sql/tabdrop.sql index 9d4138a741..6bebb3a1ed 100644 --- a/modules/miscutil/sql/tabdrop.sql +++ b/modules/miscutil/sql/tabdrop.sql @@ -450,6 +450,7 @@ DROP TABLE IF EXISTS basket_record; DROP TABLE IF EXISTS record; DROP TABLE IF EXISTS user_query_basket; DROP TABLE IF EXISTS cmtRECORDCOMMENT; +DROP TABLE IF EXISTS cmtRECORDCOMMENT_bibdoc; DROP TABLE IF EXISTS cmtCOLLAPSED; DROP TABLE IF EXISTS knwKB; DROP TABLE IF EXISTS knwKBRVAL; diff --git a/modules/webcomment/lib/Makefile.am b/modules/webcomment/lib/Makefile.am index f8a852e4fe..3f2d60ec9d 100644 --- a/modules/webcomment/lib/Makefile.am +++ b/modules/webcomment/lib/Makefile.am @@ -25,7 +25,8 @@ pylib_DATA = webcomment_config.py \ webcommentadminlib.py \ webcomment_regression_tests.py \ webcomment_washer.py \ - webcomment_web_tests.py + webcomment_web_tests.py \ + webcomment_dblayer.py EXTRA_DIST = $(pylib_DATA) diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index 03d66280a8..3d59c12a23 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -19,6 +19,7 @@ """ Comments and reviews for records """ + __revision__ = "$Id$" # non Invenio imports: @@ -83,13 +84,38 @@ get_collection_reclist, \ get_colID from invenio.search_engine_utils import get_fieldvalues +from invenio.webcomment_dblayer import \ + set_comment_to_bibdoc_relation,\ + get_comment_to_bibdoc_relations try: import invenio.template webcomment_templates = invenio.template.load('webcomment') except: pass -def perform_request_display_comments_or_remarks(req, recID, display_order='od', display_since='all', nb_per_page=100, page=1, ln=CFG_SITE_LANG, voted=-1, reported=-1, subscribed=0, reviews=0, uid=-1, can_send_comments=False, can_attach_files=False, user_is_subscribed_to_discussion=False, user_can_unsubscribe_from_discussion=False, display_comment_rounds=None): + +def perform_request_display_comments_or_remarks( + req, + recID, + display_order='od', + display_since='all', + nb_per_page=100, + page=1, + ln=CFG_SITE_LANG, + voted=-1, + reported=-1, + subscribed=0, + reviews=0, + uid=-1, + can_send_comments=False, + can_attach_files=False, + user_is_subscribed_to_discussion=False, + user_can_unsubscribe_from_discussion=False, + display_comment_rounds=None, + filter_for_text=None, + filter_for_file=None, + relate_to_file=None +): """ Returns all the comments (reviews) of a specific internal record or external basket record. @param recID: record id where (internal record IDs > 0) or (external basket record IDs < -100) @@ -165,6 +191,11 @@ def perform_request_display_comments_or_remarks(req, recID, display_order='od', nb_reviews = get_nb_reviews(recID, count_deleted=False) nb_comments = get_nb_comments(recID, count_deleted=False) + res_related_files = get_comment_to_bibdoc_relations(recID) + related_files = {} + if res_related_files: + for related_file in res_related_files: + related_files[related_file['id_comment']] = related_file # checking non vital arguemnts - will be set to default if wrong #if page <= 0 or page.lower() != 'all': @@ -311,25 +342,34 @@ def perform_request_display_comments_or_remarks(req, recID, display_order='od', display_comment_rounds.append(grouped_comments[-1][0]) display_comment_rounds.remove('latest') - body = webcomment_templates.tmpl_get_comments(req, - recID, - ln, - nb_per_page, page, last_page, - display_order, display_since, - CFG_WEBCOMMENT_ALLOW_REVIEWS, - grouped_comments, nb_comments, avg_score, - warnings, - border=0, - reviews=reviews, - total_nb_reviews=nb_reviews, - uid=uid, - can_send_comments=can_send_comments, - can_attach_files=can_attach_files, - user_is_subscribed_to_discussion=\ - user_is_subscribed_to_discussion, - user_can_unsubscribe_from_discussion=\ - user_can_unsubscribe_from_discussion, - display_comment_rounds=display_comment_rounds) + body = webcomment_templates.tmpl_get_comments( + req, + recID, + ln, + nb_per_page, + page, + last_page, + display_order, + display_since, + CFG_WEBCOMMENT_ALLOW_REVIEWS, + grouped_comments, + nb_comments, + avg_score, + warnings, + border=0, + reviews=reviews, + total_nb_reviews=nb_reviews, + uid=uid, + can_send_comments=can_send_comments, + can_attach_files=can_attach_files, + user_is_subscribed_to_discussion=user_is_subscribed_to_discussion, + user_can_unsubscribe_from_discussion=user_can_unsubscribe_from_discussion, + display_comment_rounds=display_comment_rounds, + filter_for_text=filter_for_text, + filter_for_file=filter_for_file, + relate_to_file=relate_to_file, + related_files=related_files + ) return body def perform_request_vote(cmt_id, client_ip_address, value, uid=-1): @@ -926,10 +966,21 @@ def get_reply_order_cache_data(comid): return "%s%s%s%s" % (chr((comid >> 24) % 256), chr((comid >> 16) % 256), chr((comid >> 8) % 256), chr(comid % 256)) -def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", - note="", score=0, priority=0, - client_ip_address='', editor_type='textarea', - req=None, reply_to=None, attached_files=None): +def query_add_comment_or_remark( + reviews=0, + recID=0, + uid=-1, + msg="", + note="", + score=0, + priority=0, + client_ip_address='', + editor_type='textarea', + req=None, + reply_to=None, + attached_files=None, + relate_file=None +): """ Private function Insert a comment/review or remarkinto the database @@ -942,6 +993,9 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", @param editor_type: the kind of editor used to submit the comment: 'textarea', 'ckeditor' @param req: request object. If provided, email notification are sent after we reply to user request. @param reply_to: the id of the comment we are replying to with this inserted comment. + @param relate_file: dictionary containing all the information about the + relation of the comment being submitted to the bibdocfile that was + chosen. ("id_bibdoc:version:mime") @return: integer >0 representing id if successful, integer 0 if not """ @@ -1009,6 +1063,17 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", if res: new_comid = int(res) move_attached_files_to_storage(attached_files, recID, new_comid) + + # Relate comment with a bibdocfile if one was chosen + if relate_file and len(relate_file.split(":")) >= 2: + id_bibdoc, doc_version = relate_file.split(":")[:2] + set_comment_to_bibdoc_relation( + recID, + new_comid, + id_bibdoc, + doc_version + ) + parent_reply_order = run_sql("""SELECT reply_order_cached_data from cmtRECORDCOMMENT where id=%s""", (reply_to,)) if not parent_reply_order or parent_reply_order[0][0] is None: # This is not a reply, but a first 0-level comment @@ -1617,23 +1682,26 @@ def calculate_avg_score(res): avg_score = 5.0 return avg_score -def perform_request_add_comment_or_remark(recID=0, - uid=-1, - action='DISPLAY', - ln=CFG_SITE_LANG, - msg=None, - score=None, - note=None, - priority=None, - reviews=0, - comID=0, - client_ip_address=None, - editor_type='textarea', - can_attach_files=False, - subscribe=False, - req=None, - attached_files=None, - warnings=None): +def perform_request_add_comment_or_remark( + recID=0, + uid=-1, + action='DISPLAY', + ln=CFG_SITE_LANG, + msg=None, + score=None, + note=None, + priority=None, + reviews=0, + comID=0, + client_ip_address=None, + editor_type='textarea', + can_attach_files=False, + subscribe=False, + req=None, + attached_files=None, + warnings=None, + relate_file=None +): """ Add a comment/review or remark @param recID: record id @@ -1693,7 +1761,16 @@ def perform_request_add_comment_or_remark(recID=0, if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, nickname, ln, msg, score, note, warnings, can_attach_files=can_attach_files) elif not reviews and CFG_WEBCOMMENT_ALLOW_COMMENTS: - return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, can_attach_files=can_attach_files) + return webcomment_templates.tmpl_add_comment_form( + recID, + uid, + nickname, + ln, + msg, + warnings, + can_attach_files=can_attach_files, + relate_to_file=relate_file + ) else: try: raise InvenioWebCommentError(_('Comments on records have been disallowed by the administrator.')) @@ -1767,7 +1844,18 @@ def perform_request_add_comment_or_remark(recID=0, ) ) - return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, textual_msg, can_attach_files=can_attach_files, reply_to=comID) + return webcomment_templates.tmpl_add_comment_form( + recID, + uid, + nickname, + ln, + msg, + warnings, + textual_msg=textual_msg, + can_attach_files=can_attach_files, + reply_to=comID, + relate_to_file=relate_file + ) else: try: @@ -1806,12 +1894,20 @@ def perform_request_add_comment_or_remark(recID=0, if len(warnings) == 0: if reviews: if check_user_can_review(recID, client_ip_address, uid): - success = query_add_comment_or_remark(reviews, recID=recID, uid=uid, msg=msg, - note=note, score=score, priority=0, - client_ip_address=client_ip_address, - editor_type=editor_type, - req=req, - reply_to=comID) + success = query_add_comment_or_remark( + reviews, + recID=recID, + uid=uid, + msg=msg, + note=note, + score=score, + priority=0, + client_ip_address=client_ip_address, + editor_type=editor_type, + req=req, + reply_to=comID, + relate_file=relate_file + ) else: try: raise InvenioWebCommentWarning(_('You already wrote a review for this record.')) @@ -1822,13 +1918,21 @@ def perform_request_add_comment_or_remark(recID=0, success = 1 else: if check_user_can_comment(recID, client_ip_address, uid): - success = query_add_comment_or_remark(reviews, recID=recID, uid=uid, msg=msg, - note=note, score=score, priority=0, - client_ip_address=client_ip_address, - editor_type=editor_type, - req=req, - - reply_to=comID, attached_files=attached_files) + success = query_add_comment_or_remark( + reviews, + recID=recID, + uid=uid, + msg=msg, + note=note, + score=score, + priority=0, + client_ip_address=client_ip_address, + editor_type=editor_type, + req=req, + reply_to=comID, + attached_files=attached_files, + relate_file=relate_file + ) if success > 0 and subscribe: subscribe_user_to_discussion(recID, uid) else: @@ -1855,7 +1959,16 @@ def perform_request_add_comment_or_remark(recID=0, if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, nickname, ln, msg, score, note, warnings, can_attach_files=can_attach_files) else: - return webcomment_templates.tmpl_add_comment_form(recID, uid, nickname, ln, msg, warnings, can_attach_files=can_attach_files) + return webcomment_templates.tmpl_add_comment_form( + recID, + uid, + nickname, + ln, + msg, + warnings, + can_attach_files=can_attach_files, + relate_to_file=relate_file + ) # unknown action send to display else: try: @@ -1867,7 +1980,16 @@ def perform_request_add_comment_or_remark(recID=0, if reviews and CFG_WEBCOMMENT_ALLOW_REVIEWS: return webcomment_templates.tmpl_add_comment_form_with_ranking(recID, uid, ln, msg, score, note, warnings, can_attach_files=can_attach_files) else: - return webcomment_templates.tmpl_add_comment_form(recID, uid, ln, msg, warnings, can_attach_files=can_attach_files) + return webcomment_templates.tmpl_add_comment_form( + recID, + uid, + nickname, + ln, + msg, + warnings, + can_attach_files=can_attach_files, + relate_to_file=relate_file + ) return '' diff --git a/modules/webcomment/lib/webcomment_dblayer.py b/modules/webcomment/lib/webcomment_dblayer.py new file mode 100644 index 0000000000..83c889138c --- /dev/null +++ b/modules/webcomment/lib/webcomment_dblayer.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# This file is part of Invenio. +# Copyright (C) 2014 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. + +""" WebComment database layer """ + +__revision__ = "$Id$" + +from invenio.dbquery import run_sql +from invenio.bibdocfile import BibRecDocs + + +def get_comment_to_bibdoc_relations(recID): + """ + Retrieve all comment to to bibdoc relations for the given record. + + and bibdocfiles they refer to. + :param recID: Id of the record + :return: correlations between comments and bibdocfiles + """ + query = """ + SELECT id_bibrec, + id_cmtRECORDCOMMENT, + id_bibdoc, + version + FROM cmtRECORDCOMMENT_bibdoc + WHERE id_bibrec = %s + """ + + comments_to_bibdoc = run_sql(query, (recID,), with_dict=True) + brd = BibRecDocs(recID) + bds = brd.list_bibdocs() + res = [] + for bd in bds: + for comments in comments_to_bibdoc: + if comments['id_bibdoc'] == bd.id: + res.append({ + 'id_comment': comments['id_cmtRECORDCOMMENT'], + 'id_bibdoc': bd.id, + 'version': comments['version'], + 'docname': brd.get_docname(bd.id), + }) + return res + + +def set_comment_to_bibdoc_relation(redID, cmtID, bibdocfileID, version): + """ + Set a comment to bibdoc relation. + + :param redID: Id of the record + :param cmtID: Id of the comment + :param bibdocfileID: Id of the bibdocfile + """ + + query = """ + INSERT INTO cmtRECORDCOMMENT_bibdoc + (id_bibrec, + id_cmtRECORDCOMMENT, + id_bibdoc, + version) + VALUES (%s, + %s, + %s, + %s) + """ + + parameters = (redID, cmtID, bibdocfileID, version) + + result = run_sql(query, parameters) + + return result diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index c1e308d1a6..bf6a118e7f 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -25,6 +25,7 @@ import re import html2text import markdown2 +import json # Invenio imports from invenio.webcomment_config import \ @@ -61,6 +62,7 @@ from invenio.bibformat import format_record from invenio.access_control_engine import acc_authorize_action from invenio.access_control_admin import acc_get_user_roles_from_user_info, acc_get_role_id +from invenio.bibdocfile import BibRecDocs from invenio.search_engine_utils import get_fieldvalues class Template: @@ -377,7 +379,29 @@ def tmpl_get_first_comments_with_ranking(self, recID, ln, comments=None, nb_comm write_button_form) return out - def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_creation, body, body_format, status, nb_reports, reply_link=None, report_link=None, undelete_link=None, delete_links=None, unreport_link=None, recID=-1, com_id='', attached_files=None, collapsed_p=False, admin_p=False): + def tmpl_get_comment_without_ranking( + self, + req, + ln, + nickname, + comment_uid, + date_creation, + body, + body_format, + status, + nb_reports, + reply_link=None, + report_link=None, + undelete_link=None, + delete_links=None, + unreport_link=None, + recID=-1, + com_id='', + attached_files=None, + collapsed_p=False, + admin_p=False, + related_files=None + ): """ private function @param req: request object to fetch user info @@ -399,6 +423,7 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ @param com_id: ID of the comment displayed @param attached_files: list of attached files @param collapsed_p: if the comment should be collapsed or not + @param related_files: Display related files @return: html table of comment """ from invenio.search_engine import guess_primary_collection_of_a_record @@ -485,6 +510,20 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ 'toggle_url': create_url(CFG_SITE_URL + '/' + CFG_SITE_RECORD + '/' + str(recID) + '/comments/toggle', {'comid': com_id, 'ln': ln, 'collapse': collapsed_p and '0' or '1', 'referer': user_info['uri']}), 'collapse_ctr_class': collapsed_p and 'webcomment_collapse_ctr_right' or 'webcomment_collapse_ctr_down', 'collapse_label': collapsed_p and _("Open") or _("Close")} + related_file_element = '' + try: + related_file = related_files[com_id] + related_file_element = """ + <div class="cmt_file_relation" doc_code="%(id_bibdoc)s:%(version)s" style="float:right"> + This comment is related with file %(docname)s, version %(version)s + </div> + """ % { + 'docname': related_file['docname'], + 'version': related_file['version'], + 'id_bibdoc': related_file['id_bibdoc'] + } + except (TypeError, KeyError): + pass out += """ <div class="webcomment_comment_box"> @@ -495,6 +534,7 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ %(title)s <div class="webcomment_comment_date">%(date)s</div> <a class="webcomment_permalink" title="Permalink to this comment" href="#C%(comid)s">¶</a> + %(related_file_element)s </div> <div class="collapsible_content" id="collapsible_content_%(comid)s" style="%(collapsible_content_style)s"> <div class="webcomment_comment_body"> @@ -517,10 +557,33 @@ def tmpl_get_comment_without_ranking(self, req, ln, nickname, comment_uid, date_ 'comid': com_id, 'collapsible_content_style': collapsed_p and 'display:none' or '', 'toggle_visibility_block': toggle_visibility_block, + 'related_file_element': related_file_element } return out - def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_creation, body, body_format, status, nb_reports, nb_votes_total, nb_votes_yes, star_score, title, report_link=None, delete_links=None, undelete_link=None, unreport_link=None, recID=-1, admin_p=False): + def tmpl_get_comment_with_ranking( + self, + req, + ln, + nickname, + comment_uid, + date_creation, + body, + body_format, + status, + nb_reports, + nb_votes_total, + nb_votes_yes, + star_score, + title, + report_link=None, + delete_links=None, + undelete_link=None, + unreport_link=None, + recID=-1, + admin_p=False, + related_files=None + ): """ private function @param req: request object to fetch user info @@ -539,6 +602,7 @@ def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_cre @param delete_link: http link to delete the message @param unreport_link: http link to unreport the comment @param recID: recID where the comment is posted + @param related_files: Related comment files @return: html table of review """ from invenio.search_engine import guess_primary_collection_of_a_record @@ -604,10 +668,26 @@ def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_cre else: _body = '<div class="webcomment_review_pending_approval_message">This review is pending approval due to user reports.</div>' links = '' + related_file_element = '' + try: + related_file = related_files[com_id] + related_file_element = """ + <div class="cmt_file_relation" doc_code="%(id_bibdoc)s:%(version)s" style="float:right"> + This comment is related with file %(docname)s, version %(version)s + </div> + """ % { + 'docname': related_file['docname'], + 'version': related_file['version'], + 'id_bibdoc': related_file['id_bibdoc'] + } + except (TypeError, KeyError): + pass + out += ''' <div class="webcomment_review_box"> <div class="webcomment_review_box_inner"> + %(related_file_element)s <img src="%(baseurl)s/img/%(star_score_img)s" alt="%(star_score)s/> <div class="webcomment_review_title">%(title)s</div> <div class="webcomment_review_label_reviewed">%(reviewed_label)s</div> @@ -622,7 +702,8 @@ def tmpl_get_comment_with_ranking(self, req, ln, nickname, comment_uid, date_cre 'reviewed_label': reviewed_label, 'useful_label' : useful_label, 'body' : _body, - 'abuse' : links + 'abuse' : links, + 'related_file_element' : related_file_element } return out @@ -635,12 +716,16 @@ def tmpl_get_comments(self, req, recID, ln, warnings, border=0, reviews=0, total_nb_reviews=0, - nickname='', uid=-1, note='',score=5, + nickname='', uid=-1, note='', score=5, can_send_comments=False, can_attach_files=False, user_is_subscribed_to_discussion=False, user_can_unsubscribe_from_discussion=False, - display_comment_rounds=None): + display_comment_rounds=None, + filter_for_text=None, + filter_for_file=None, + relate_to_file=None, + related_files=None): """ Get table of all comments @param recID: record id @@ -670,6 +755,7 @@ def tmpl_get_comments(self, req, recID, ln, @param can_attach_files: boolean, if user can attach file to comment or not @param user_is_subscribed_to_discussion: True if user already receives new comments by email @param user_can_unsubscribe_from_discussion: True is user is allowed to unsubscribe from discussion + @param related_files: Realated comment files """ # load the right message language _ = gettext_set_language(ln) @@ -727,7 +813,17 @@ def tmpl_get_comments(self, req, recID, ln, discussion = 'comments' comments_link = '<strong>%s (%i)</strong>' % (_('Comments'), total_nb_comments) reviews_link = '<a href="%s/%s/%s/reviews/">%s</a> (%i)' % (CFG_SITE_URL, CFG_SITE_RECORD, recID, _('Reviews'), total_nb_reviews) - add_comment_or_review = self.tmpl_add_comment_form(recID, uid, nickname, ln, note, warnings, can_attach_files=can_attach_files, user_is_subscribed_to_discussion=user_is_subscribed_to_discussion) + add_comment_or_review = self.tmpl_add_comment_form( + recID, + uid, + nickname, + ln, + note, + warnings, + can_attach_files=can_attach_files, + user_is_subscribed_to_discussion=user_is_subscribed_to_discussion, + relate_to_file=relate_to_file + ) # voting links useful_dict = { 'siteurl' : CFG_SITE_URL, @@ -876,18 +972,74 @@ def tmpl_get_comments(self, req, recID, ln, if not reviews: report_link = '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/report?ln=%(ln)s&amp;comid=%%(comid)s&amp;do=%(do)s&amp;ds=%(ds)s&amp;nb=%(nb)s&amp;p=%(p)s&amp;referer=%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/display' % useful_dict % {'comid':comment[c_id]} reply_link = '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/add?ln=%(ln)s&amp;action=REPLY&amp;comid=%%(comid)s' % useful_dict % {'comid':comment[c_id]} + related_file = related_files.get(comment[c_id], None) + if related_file is not None: + related_file_id_bibdoc = related_file.get( + "id_bibdoc", + None + ) + related_file_version = related_file.get( + "version", + None + ) + if related_file_id_bibdoc is not None and \ + related_file_version is not None: + reply_link += "&amp;relate_file={0}:{1}".format( + related_file_id_bibdoc, + related_file_version + ) delete_links['mod'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_mod?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) delete_links['auth'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_auth?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) undelete_link = "%s/admin/webcomment/webcommentadmin.py/undel_com?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) unreport_link = "%s/admin/webcomment/webcommentadmin.py/unreport_com?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) - comments_rows += self.tmpl_get_comment_without_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_body_format], comment[c_status], comment[c_nb_reports], reply_link, report_link, undelete_link, delete_links, unreport_link, recID, comment[c_id], files, comment[c_visibility]) + comments_rows += self.tmpl_get_comment_without_ranking( + req, + ln, + messaging_link, + comment[c_user_id], + comment[c_date_creation], + comment[c_body], + comment[c_body_format], + comment[c_status], + comment[c_nb_reports], + reply_link, + report_link, + undelete_link, + delete_links, + unreport_link, + recID, + comment[c_id], + files, + comment[c_visibility], + related_files=related_files + ) else: report_link = '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/reviews/report?ln=%(ln)s&amp;comid=%%(comid)s&amp;do=%(do)s&amp;ds=%(ds)s&amp;nb=%(nb)s&amp;p=%(p)s&amp;referer=%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/reviews/display' % useful_dict % {'comid': comment[c_id]} delete_links['mod'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_mod?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) delete_links['auth'] = "%s/admin/webcomment/webcommentadmin.py/del_single_com_auth?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) undelete_link = "%s/admin/webcomment/webcommentadmin.py/undel_com?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) unreport_link = "%s/admin/webcomment/webcommentadmin.py/unreport_com?ln=%s&amp;id=%s" % (CFG_SITE_URL, ln, comment[c_id]) - comments_rows += self.tmpl_get_comment_with_ranking(req, ln, messaging_link, comment[c_user_id], comment[c_date_creation], comment[c_body], comment[c_body_format], comment[c_status], comment[c_nb_reports], comment[c_nb_votes_total], comment[c_nb_votes_yes], comment[c_star_score], comment[c_title], report_link, delete_links, undelete_link, unreport_link, recID) + comments_rows += self.tmpl_get_comment_with_ranking( + req, + ln, + messaging_link, + comment[c_user_id], + comment[c_date_creation], + comment[c_body], + comment[c_body_format], + comment[c_status], + comment[c_nb_reports], + comment[c_nb_votes_total], + comment[c_nb_votes_yes], + comment[c_star_score], + comment[c_title], + report_link, + delete_links, + undelete_link, + unreport_link, + recID, + related_files=related_files + ) helpful_label = _("Was this review helpful?") report_abuse_label = "(" + _("Report abuse") + ")" yes_no_separator = '<td> / </td>' @@ -1029,6 +1181,9 @@ def tmpl_get_comments(self, req, recID, ln, body = ''' %(comments_and_review_tabs)s %(subscription_link_before)s +%(filtering_script)s +<div style="clear:both"></div> +<br /> <!-- start comments table --> <div class="webcomment_comment_table"> %(comments_rows)s @@ -1068,7 +1223,14 @@ def tmpl_get_comments(self, req, recID, ln, not reviews and total_nb_comments != 0 and subscription_link or "", - 'subscription_link_after': subscription_link + 'subscription_link_after': subscription_link, + 'filtering_script': self.tmpl_comment_filtering_box_and_script( + recID=recID, + filter_text=filter_for_text, + filter_file=filter_for_file, + page=page, + nb_per_page=nb_per_page, + nb_pages=nb_pages) } # form is not currently used. reserved for an eventual purpose @@ -1266,9 +1428,20 @@ def tmpl_error(self, error, ln=CFG_SITE_LANG): errorbox += "</div><br />\n" return errorbox - def tmpl_add_comment_form(self, recID, uid, nickname, ln, msg, - warnings, textual_msg=None, can_attach_files=False, - user_is_subscribed_to_discussion=False, reply_to=None): + def tmpl_add_comment_form( + self, + recID, + uid, + nickname, + ln, + msg, + warnings, + textual_msg=None, + can_attach_files=False, + user_is_subscribed_to_discussion=False, + reply_to=None, + relate_to_file=None + ): """ Add form for comments @param recID: record id @@ -1373,18 +1546,34 @@ def tmpl_add_comment_form(self, recID, uid, nickname, ln, msg, # Offer to subscribe to discussion subscribe_to_discussion = '<small><input type="checkbox" name="subscribe" id="subscribe"/><label for="subscribe">%s</label></small>' % _("Send me an email when a new comment is posted") - form = """<div id="comment-write"><h2>%(add_comment)s</h2> + relate_file_element = "" + relate_file_selector = self.tmpl_bibdocfile_selector_element( + recID, + select_file_title="Select a file revision", + selected_file=relate_to_file, + ln=ln + ) + # Relate a to a file element + if relate_file_selector: + relate_file_title = ( + "Relate this comment to an existing file revision" + ) + relate_file_element = ( + "<div id='relate_file'><small>{0}</small>" + "{1}</div>").format(relate_file_title, relate_file_selector) -%(editor)s -<br /> -%(simple_attach_file_interface)s - <span class="reportabuse">%(note)s</span> + form = """<div id="comment-write"><h2>%(add_comment)s</h2> + %(editor)s + %(relate_file_element)s + %(simple_attach_file_interface)s <div class="submit-area"> - %(subscribe_to_discussion)s<br /> + <br /> + <span class="reportabuse">%(note)s</span> <br /> <br /> + %(subscribe_to_discussion)s<br /><br /> <input class="adminbutton" type="submit" value="Add comment" onclick="user_must_confirm_before_leaving_page = false;return true;"/> %(reply_to)s </div> -</div> + </div> """ % {'note': note, 'record_label': _("Article") + ":", 'comment_label': _("Comment") + ":", @@ -1392,11 +1581,13 @@ def tmpl_add_comment_form(self, recID, uid, nickname, ln, msg, 'editor': editor, 'subscribe_to_discussion': subscribe_to_discussion, 'reply_to': reply_to and '<input type="hidden" name="comid" value="%s"/>' % reply_to or '', - 'simple_attach_file_interface': simple_attach_file_interface} + 'simple_attach_file_interface': simple_attach_file_interface, + 'relate_file_element': relate_file_element + } form_link = "%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/%(function)s?%(arguments)s" % link_dic form = self.create_write_comment_hiddenform(action=form_link, method="post", text=form, button='Add comment', enctype='multipart/form-data', form_id='cmtForm', - form_name='cmtForm') + form_name='cmtForm', relate_file='') return warnings + form + self.tmpl_page_do_not_leave_comment_page_js(ln=ln) @@ -2917,3 +3108,336 @@ def tmpl_prepare_comment_body(self, body, body_format, output_format): body = cgi.escape(body) return body + + def tmpl_comment_filtering_box_and_script( + self, + recID=None, + filter_text='', + filter_file=',', + page=1, + nb_per_page=100, + nb_pages=1, + ln=CFG_SITE_LANG + ): + """Filter box for comments. + + :param string more_exist_p: If there are more comments + :param string filter_text: The query filter + :param int total_nb: Total number of comments + :param int recID: The record id + :param string ln: The language + :param int page: the current page + :param int nb_per_page: Number of comments per page + :param int nb_pages: Total pages + :param str ln: The current language + """ + + # load the right message language + _ = gettext_set_language(ln) + more_exist_p = False + # Calculate next page + next_page = page + 1 + if nb_pages >= next_page: + more_exist_p = True + + # Create filter all url + filter_next_page_url = ( + "/record/{0}/comments/display?ln={1}&nb={2}&p={3}" + ).format(recID, ln, nb_per_page, next_page) + + select_element_with_files = self.tmpl_bibdocfile_selector_element( + recID, + only_optgroup_elements_p=True, + ln=ln + ) + # Prepare the filter element + select_element_with_files_filter = "" + if select_element_with_files: + select_element_with_files_filter = ( + "<div class='filter-area-advance'><select style='display:none'" + " id='selectize_input'><option value="">select a revision file" + " to filter comments</option>{0}</select><a href='#' " + "data-state='closed' class='toggle-advance-search'>advanced " + "filtering</a></div>" + ).format(select_element_with_files) + + filter_element = """ + <script src="%(CFG_SITE_URL)s/js/jquery.ba-throttle-debounce.min.js" type="text/javascript"></script> + <script src="%(CFG_SITE_URL)s/js/jquery.highlight.min.js" type="text/javascript"></script> + <style> + .highlight { background-color: yellow; } + </style> + <script type="text/javascript"> + $(document).ready(function(){ + var visual_effect_time = 10; + // The comment box wrapper, should be string with the identification + // class or id + var comment_wrapper = '.webcomment_comment_box'; + var matches_alert = '.webcomments-matches-alert'; + var filter_all_url = "%(filter_next_page_url)s"; + var selected_file = "%(filter_file)s"; + var matching_comments_number = $("#matching_comments_number"); + + var filter_input = $("#filter_input"); + var filter_area = $("#filter_area"); + var no_matches_class = 'filter-no-matches'; + var selectize_input = $('#selectize_input'); + + selectize_input.on('change', filter); + var alert_div = "<div class='webcomments-matches-alert' style='display:none'></div>"; + // Prepend no matching div + var no_results_alert = "No matches in this page. <a class='clear' href='#'>Clear the filter</a> to see all comments"; + var results_alert = "<a class='clear' href='#'>Clear the filter</a> to see all comments"; + var next_url_alert = " or <a href='#' class='see'>see the matching in the next page</a>."; + $('#cmtRound').append(alert_div); + // Register clear filter + $(matches_alert).on('click', 'a.clear', function(e){ + e.preventDefault(); + filter_input.val(''); + selectize_input.val(''); + filter_input.trigger('keyup'); + }); + // Advance search handler + var advanced_search_button = $('.toggle-advance-search'); + advanced_search_button.on('click', function(e){ + e.preventDefault(); + selectize_input.toggle(0); + advanced_search_button.remove(); + }); + + // add icontains to jquery selector expression + jQuery.expr[':'].icontains = function(a, i, m) { + return jQuery(a).text().toUpperCase() + .indexOf(m[3].toUpperCase()) >= 0; + }; + + // Bind the event to input + filter_input.keyup($.debounce(250, filter)); + + if(selected_file){ + selectize_input.find("option").each(function() { + if($(this).val() == selected_file){ + } + }).prop('selected', true); + selectize_input.val(selected_file); + advanced_search_button.trigger('click'); + filter_input.trigger("keyup"); + } + + // Create the filter function + function filter(){ + var comments = $(".collapsible_content") + .children() + .not(".webcomment_comment_options"); + var query = filter_input.val().toLowerCase(); + var filter_file = selectize_input.val(); + // All comments anchor + var next_url = filter_all_url+'&filter_text='+ + query+'&filter_file='+filter_file; + + // Unhightlight all comments + comments.unhighlight(); + $("#search_next_page").remove(); + matching_comments_number.hide(0); + $(matches_alert).hide(0); + var comment_count = 0; + // If the query is empty just return + if (query == "" && selectize_input.val()== ""){ + comments + .closest(comment_wrapper) + .show(visual_effect_time); + return; + } + // If the query is not empty and no file has been selected + if (query != "" && selectize_input.val()== ""){ + var relevant_comments = $('.collapsible_content') + .find('.webcomment_comment_body'); + + var matching_comments = relevant_comments + .filter(":icontains('" + query + "')") + + var not_matching_comments = relevant_comments + .filter(":not(:icontains('" + query + "'))") + + // Highlight matched + matching_comments.highlight(query); + // Show them + matching_comments + .closest(comment_wrapper) + .show(visual_effect_time); + // Hide them + not_matching_comments + .closest(comment_wrapper) + .hide(visual_effect_time); + // Get comment count + comment_count = matching_comments.length; + } + // If a file has been selected + if (selectize_input.val()!= ""){ + var comments_not_contain_the_doc = $(".cmt_file_relation").not("[doc_code='" + selectize_input.val() + "']"); + var comments_without_doc = $(".webcomment_comment_title:not(:has(.cmt_file_relation))"); + // Hide comments in both cases + $(comments_not_contain_the_doc.closest(comment_wrapper)) + .add(comments_without_doc.closest(comment_wrapper)) + .hide(visual_effect_time); + + // Get all the related comments with this doc + var related_docs = $(".cmt_file_relation[doc_code='" + selectize_input.val() + "']") + .closest(comment_wrapper) + .find('.webcomment_comment_body'); + + // Get comments that are not contain the query + var non_relevant_docs = related_docs.filter(":not(:icontains('" + query + "'))"); + + // Get comments that contain the query + var relevant_docs = related_docs.filter(":icontains('" + query + "')"); + + // Highlight comments containing the query + relevant_docs.highlight(query); + + // Hide all not relevant comments + non_relevant_docs + .closest(comment_wrapper) + .hide(visual_effect_time); + // Show only relevant comments + relevant_docs + .closest(comment_wrapper) + .show(visual_effect_time); + // Count the relevant comments + comment_count = relevant_docs.length; + } + if (comment_count > 0){ + matching_comments_number.removeClass(no_matches_class); + matching_comments_number.text(comment_count + " matching comment(s)"); + $(matches_alert).html(results_alert); + }else{ + matching_comments_number.addClass(no_matches_class); + matching_comments_number.text("No matches."); + $(matches_alert).html(no_results_alert); + } + matching_comments_number.show(0); + + if (%(has_more_pages)s){ + $(matches_alert).append(next_url_alert); + }else{ + $(matches_alert).append('.'); + } + $(matches_alert).find('.see').attr('href', next_url); + $(matches_alert).show(0); + } + if ("%(filter_text)s"){ + filter_input.val("%(filter_text)s"); + filter_input.trigger("keyup"); + } + }); + </script> + + <div id="filter_area"> + <p class="filter-area-count"><label id="matching_comments_number"></p> + <input id="filter_input" placeholder="%(filter_placeholder)s" value="" /> + %(select_element_with_files)s + </div> + """ % {'has_more_pages': more_exist_p and "true" or "false", + 'filter_placeholder': _('filter comments in this page'), + 'filter_label': _('Filter') + ':&nbsp;', + 'filter_text': filter_text, + 'filter_file': filter_file, + 'filter_next_page_url': filter_next_page_url, + 'select_element_with_files': select_element_with_files_filter, + 'CFG_SITE_URL': CFG_SITE_URL + } + + return filter_element + + def tmpl_bibdocfile_selector_element( + self, + recID, + only_optgroup_elements_p=False, + select_file_title='Select a file', + selected_file=None, + ln=CFG_SITE_LANG + ): + """Selector element for bibdocfiles of a record. + + :param int reciID: The record id + :param bool only_optgroup_elements_p: Show only top level files + :param str select_file_title: The default file title + :param str ln: The current language + """ + _ = gettext_set_language(ln) + + docfiles = [] + brd = BibRecDocs(recID) + bds = brd.list_bibdocs() + for bd in bds: + for version in bd.list_versions(): + docfiles.append({ + 'version': version, + 'id_bibdoc': bd.id, + 'docname': brd.get_docname(bd.id) + }) + + if not docfiles: + return "" + + optgroups_html = '' + optgroups = {} + + # group files in a dictionary using the file name as a key + for docfile in docfiles: + if not optgroups.get(docfile['docname']): + optgroups[docfile['docname']] = [] + optgroups[docfile['docname']].append(docfile) + + # construct the optgroups to be displayed + for key, values in optgroups.iteritems(): + optgroups_html += "<optgroup label='{0}'>".format(key) + for docfile in values: + file_id_version = "{file_id}:{version}".format( + file_id=docfile['id_bibdoc'], + version=docfile['version'] + ) + is_selected = \ + selected_file == file_id_version \ + and " selected" \ + or "" + optgroups_html += ( + "<option value='{file_id_version}'{is_selected}>" + "{file_name}, version {version}</option>" + ).format( + file_id_version=file_id_version, + is_selected=is_selected, + file_name=key, + version=docfile['version'] + ) + optgroups_html += "</optgroup>" + + if only_optgroup_elements_p: + return optgroups_html + + script_element = """ + <script type="text/javascript"> + $(document).ready(function(){ + $("#file_selector").change(function() { + $('input[name="relate_file"]').attr('value',$( "#file_selector option:selected" ).attr('value')); + }); + $("#file_selector").trigger("change"); + }); + </script> + """ + + file_selection_element = """ + <select id="file_selector"> + <option value=""%(is_selected)s>%(select_file_title)s</option> + %(optgroup_html)s + </select> + %(script_element)s + <div style="clear:both"></div> + """ % { + "optgroup_html": optgroups_html, + "script_element": script_element, + "select_file_title": select_file_title, + "is_selected": selected_file is None and " selected" or "", + } + + return file_selection_element diff --git a/modules/webcomment/lib/webcomment_webinterface.py b/modules/webcomment/lib/webcomment_webinterface.py index 000117b7a3..2601002a04 100644 --- a/modules/webcomment/lib/webcomment_webinterface.py +++ b/modules/webcomment/lib/webcomment_webinterface.py @@ -85,20 +85,31 @@ websearch_templates = invenio.template.load('websearch') import os from invenio import webinterface_handler_config as apache -from invenio.bibdocfile import \ - stream_file, \ - decompose_file, \ - propose_next_docname +from invenio.bibdocfile import ( + stream_file, decompose_file, propose_next_docname +) + class WebInterfaceCommentsPages(WebInterfaceDirectory): """Defines the set of /comments pages.""" - _exports = ['', 'display', 'add', 'vote', 'report', 'index', 'attachments', - 'subscribe', 'unsubscribe', 'toggle'] + _exports = [ + '', + 'display', + 'add', + 'vote', + 'report', + 'index', + 'attachments', + 'subscribe', + 'unsubscribe', + 'toggle', + ] def __init__(self, recid=-1, reviews=0): self.recid = recid - self.discussion = reviews # 0:comments, 1:reviews + # 0:comments, 1:reviews + self.discussion = reviews self.attachments = WebInterfaceCommentsFiles(recid, reviews) def index(self, req, form): @@ -140,7 +151,10 @@ def display(self, req, form): 'voted': (int, -1), 'reported': (int, -1), 'subscribed': (int, 0), - 'cmtgrp': (list, ["latest"]) # 'latest' is now a reserved group/round name + 'cmtgrp': (list, ["latest"]), # 'latest' is now a reserved group/round name + 'filter_text': (str, ''), + 'filter_file': (str, ''), + 'relate_file': (str, '') }) _ = gettext_set_language(argd['ln']) @@ -217,7 +231,9 @@ def display(self, req, form): (ok, problem) = check_recID_is_in_range(self.recid, check_warnings, argd['ln']) if ok: - body = perform_request_display_comments_or_remarks(req=req, recID=self.recid, + body = perform_request_display_comments_or_remarks( + req=req, + recID=self.recid, display_order=argd['do'], display_since=argd['ds'], nb_per_page=argd['nb'], @@ -232,7 +248,10 @@ def display(self, req, form): can_attach_files=can_attach_files, user_is_subscribed_to_discussion=user_is_subscribed_to_discussion, user_can_unsubscribe_from_discussion=user_can_unsubscribe_from_discussion, - display_comment_rounds=display_comment_rounds + display_comment_rounds=display_comment_rounds, + filter_for_text=argd['filter_text'], + filter_for_file=argd['filter_file'], + relate_to_file=argd['relate_file'] ) title, description, keywords = websearch_templates.tmpl_record_page_header_content(req, self.recid, argd['ln']) @@ -304,7 +323,8 @@ def add(self, req, form): 'comid': (int, 0), 'editor_type': (str, ""), 'subscribe': (str, ""), - 'cookie': (str, "") + 'cookie': (str, ""), + 'relate_file': (str, "") }) _ = gettext_set_language(argd['ln']) @@ -486,22 +506,25 @@ def add(self, req, form): # User is not already subscribed, and asked to subscribe subscribe = True - body = perform_request_add_comment_or_remark(recID=self.recid, - ln=argd['ln'], - uid=uid, - action=argd['action'], - msg=argd['msg'], - note=argd['note'], - score=argd['score'], - reviews=self.discussion, - comID=argd['comid'], - client_ip_address=client_ip_address, - editor_type=argd['editor_type'], - can_attach_files=can_attach_files, - subscribe=subscribe, - req=req, - attached_files=added_files, - warnings=warning_msgs) + body = perform_request_add_comment_or_remark( + recID=self.recid, + ln=argd['ln'], + uid=uid, + action=argd['action'], + msg=argd['msg'], + note=argd['note'], + score=argd['score'], + reviews=self.discussion, + comID=argd['comid'], + client_ip_address=client_ip_address, + editor_type=argd['editor_type'], + can_attach_files=can_attach_files, + subscribe=subscribe, + req=req, + attached_files=added_files, + warnings=warning_msgs, + relate_file=argd['relate_file'] + ) if self.discussion: title = _("Add Review") diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index b089862b59..e04a4b57f6 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -1246,7 +1246,7 @@ form input[disabled], form input[disabled="true"]{ } #comment-write .submit-area { - margin:20px 0 0 -5px; + margin: 0px 0 0 -5px; } /* Subscribe to comment*/ @@ -1964,8 +1964,7 @@ dd{ margin-bottom: 10px; } .cmtsubround { - margin: 5px 15px 5px; - border-bottom: 1px dashed #bbb; + margin: 5px 15px 5px; } .cmtfilesblock{ background-color:#f5f5f5; @@ -4054,4 +4053,99 @@ a.author_orcid_image_link { margin-right:10px; } +/* WebComment filtering and related files */ +#filter_area{ + float: right; + margin-top: -5px; + height: 20px; + display: inline-table; + text-align: right; +} + +#filter_area input{ + padding: 4px; + margin: 0; + width: 240px; + font-size: 12px; +} + +#filter_area p{ + display: inline; + padding: 0; + margin: 0; +} + +#filter_area #matching_comments_number.filter-no-matches { + color: #b85757; +} + +#filter_area p.filter-area-count { + color: #777; + font-size: 13px; + padding-right: 10px; +} + +#filter_area #selectize_input { + width: 252px; +} + +#filter_area .filter-area-advance { + margin-top: 5px; +} + +#filter_area .filter-area-advance a.toggle-advance-search { + font-size: 12px; + line-height: 20px; + vertical-align: top; + background-image: url('/img/filter_filled.png'); + background-position: 0 2px; + background-repeat: no-repeat; + padding-left: 12px; +} + +#cmtRound.cmtround { + margin-top: 80px; +} + +#cmtSubRound .cmt_file_relation { + font-size: 11px; + color: #777; + padding-right: 5px; +} + +#relate_file { + margin-bottom: 20px; + background: #f8f8f8; + border: 1px solid #ddd; + padding: 10px; + margin-top: 10px; +} + +#relate_file small{ + line-height: 25px; +} + +#relate_file select{ + min-width: 255px; +} + +#search_all_authors_anchor{ + font-size: 12px; +} + +.webcomments-matches-alert { + text-align: center; + font-size: 14px; + margin-bottom: 20px; + color: #8a6d3b; + background: #fcf8e3; + border: 1px solid #ded18a; + padding: 10px 5px; +} + +#search_next_page { + text-align: right!important; + margin-top: 20px; +} + /* end of invenio.css */ diff --git a/modules/webstyle/img/Makefile.am b/modules/webstyle/img/Makefile.am index 57bb29d3d1..adb6affd42 100644 --- a/modules/webstyle/img/Makefile.am +++ b/modules/webstyle/img/Makefile.am @@ -326,7 +326,8 @@ img_DATA = add-small.png \ yahoo_icon_24.png \ yahoo_icon_48.png \ yammer_icon_24.png \ - yammer_icon_48.png + yammer_icon_48.png \ + filter_filled.png tmpdir=$(localstatedir)/tmp diff --git a/modules/webstyle/img/filter_filled.png b/modules/webstyle/img/filter_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..6659148866d54b8a7142d7268527c87825ef3a29 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V3y@T|W;X*;Ea{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaL-l0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR z+ueoXe|!I#{XiaPfk$L9koEv$x0Bg+Kt{Qzi(?4K_1Lp}IS&Oe9QioE(#c?|LJALi z!MrPx$NUOr>nQLC-DJ^mV&iC<m_G5r_1Mz-K5MC$=Y0j<<knuXoV5DO`>^$soEMsT zOmbYS@IZ0b?H6<A_M{|;_^Msxc%8bh#KE%t&1-Yd{U4sa$~0}?a3C)G`Jz_wbGcgU z7W>}X1$2UHiEBhjN@7W>RdP`(kYX@0Ff`FMG}JXR2{ACSGBvj{Fw!-!ure@k7FhEf dMMG|WN@iLmZVg7hBGEt%44$rjF6*2UngDsscufER literal 0 HcmV?d00001 From 2c5a01685cdf2067e01ba4780b224a73570f69a1 Mon Sep 17 00:00:00 2001 From: Avraam Tsantekidis <avraam.tsantekidis@cern.ch> Date: Wed, 13 Aug 2014 15:45:35 +0200 Subject: [PATCH 43/83] WebComment: display comment submission deadline * FEATURE Displays the comment submission deadline, when present. Co-Authored-By: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Signed-off-by: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> --- modules/webcomment/lib/webcomment_config.py | 25 ++++++++++ .../webcomment/lib/webcomment_templates.py | 50 ++++++++++++++++--- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/modules/webcomment/lib/webcomment_config.py b/modules/webcomment/lib/webcomment_config.py index ba7ffac9cf..c2c6ad3f19 100644 --- a/modules/webcomment/lib/webcomment_config.py +++ b/modules/webcomment/lib/webcomment_config.py @@ -23,6 +23,8 @@ __revision__ = "$Id$" +from invenio.config import CFG_CERN_SITE + CFG_WEBCOMMENT_ACTION_CODE = { 'ADD_COMMENT': 'C', 'ADD_REVIEW': 'R', @@ -48,6 +50,29 @@ }, } +# Based on CFG_WEBCOMMENT_DEADLINE_CONFIGURATION we can display, but not +# enforce comment submission deadlines. The configuration is composed of rules +# (dictionary items). For a rule to be applied in the currently displayed +# record, the dictionary key has to be in the list of values of the MARC field +# that is the first element of the tuple in that dictionary key's value. If that +# is the case, then the dealine is retrieved as the first value of the MARC +# field that is the second element of the tuple in that dictionary key's value. +# In order to programmatically check if the deadline has passed or not we need +# to know the format of the given deadline, using standard strftime conversion +# specifications <http://linux.die.net/man/3/strftime>. The deadline format is +# the third element of the tuple in that dictionary key's value. +if CFG_CERN_SITE: + CFG_WEBCOMMENT_DEADLINE_CONFIGURATION = { + "ATLASPUBDRAFT": ( + "980__a", + "925__b", + "%d %b %Y", + ) + } +else: + CFG_WEBCOMMENT_DEADLINE_CONFIGURATION = { + } + # Exceptions: errors class InvenioWebCommentError(Exception): """A generic error for WebComment.""" diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index bf6a118e7f..38e63aa193 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -26,17 +26,22 @@ import html2text import markdown2 import json +from datetime import datetime, date -# Invenio imports from invenio.webcomment_config import \ CFG_WEBCOMMENT_BODY_FORMATS, \ CFG_WEBCOMMENT_OUTPUT_FORMATS from invenio.urlutils import create_html_link, create_url -from invenio.webuser import get_user_info, \ - collect_user_info, \ - isGuestUser, \ - get_email -from invenio.dateutils import convert_datetext_to_dategui +from invenio.webuser import ( + get_user_info, + collect_user_info, + isGuestUser, + get_email +) +from invenio.dateutils import ( + convert_datetext_to_dategui, + convert_datestruct_to_dategui +) from invenio.webmessage_mailutils import email_quoted_txt2html from invenio.config import \ CFG_SITE_URL, \ @@ -64,6 +69,7 @@ from invenio.access_control_admin import acc_get_user_roles_from_user_info, acc_get_role_id from invenio.bibdocfile import BibRecDocs from invenio.search_engine_utils import get_fieldvalues +from invenio.webcomment_config import CFG_WEBCOMMENT_DEADLINE_CONFIGURATION class Template: """templating class, refer to webcomment.py for examples of call""" @@ -1177,10 +1183,39 @@ def tmpl_get_comments(self, req, recID, ln, ' from this discussion. You will no longer receive emails about new comments.' + \ '</p>' + deadline_text = "" + for (matching_value, (matching_key, deadline_key, deadline_value_format)) in CFG_WEBCOMMENT_DEADLINE_CONFIGURATION.items(): + if matching_value in get_fieldvalues(recID, matching_key): + deadline_value = get_fieldvalues(recID, deadline_key) + if deadline_value: + try: + deadline_object = datetime.strptime(deadline_value[0], deadline_value_format) + except ValueError: + break + datetime_left = deadline_object - datetime.now() + # People can still submit comments on the day of the + # deadline, so make sure to add a day here. + days_left = datetime_left.days + 1 + deadline_struct_time = date.timetuple(deadline_object) + deadline_dategui = convert_datestruct_to_dategui(deadline_struct_time, ln).split(",")[0] + if days_left > 0: + deadline_text = _("Please submit your comments within %i days, by %s." % (days_left, deadline_dategui)) + deadline_text = "<span class=\"warninggreen\">" + deadline_text + "</span><br />" + elif days_left == 0: + deadline_text = _("Please submit your comments by today.") + deadline_text = "<span class=\"warninggreen\">" + deadline_text + "</span><br />" + else: + deadline_text = _("The deadline to submit comments expired %i days ago, on %s." % (abs(days_left), deadline_dategui)) + deadline_text = "<span class=\"warningred\">" + deadline_text + "</span><br />" + # If at least one matching value is found, then there is no + # need to look for more. + break + # do NOT remove the HTML comments below. Used for parsing body = ''' %(comments_and_review_tabs)s %(subscription_link_before)s +%(deadline_text)s %(filtering_script)s <div style="clear:both"></div> <br /> @@ -1230,7 +1265,8 @@ def tmpl_get_comments(self, req, recID, ln, filter_file=filter_for_file, page=page, nb_per_page=nb_per_page, - nb_pages=nb_pages) + nb_pages=nb_pages), + 'deadline_text': deadline_text, } # form is not currently used. reserved for an eventual purpose From f228069723ed854edff3283504b3f260bdd7dc4e Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis <me@drjova.com> Date: Fri, 21 Nov 2014 15:19:55 +0100 Subject: [PATCH 44/83] WebComment: Enable a custom checkbox for comments * FEATURE When writing comments, an extra checkbox can be displayed based on a configuration. If checked, it populates the `title` field of the given comment. Co-Authored-By: Avraam Tsantekidis <avraam.tsantekidis@cern.ch> Signed-off-by: Harris Tzovanakis <me@drjova.com> --- modules/webcomment/lib/webcomment.py | 9 +- modules/webcomment/lib/webcomment_config.py | 95 +++++++++-- .../webcomment/lib/webcomment_templates.py | 160 +++++++++++------- .../webcomment/lib/webcomment_webinterface.py | 32 ++-- 4 files changed, 206 insertions(+), 90 deletions(-) diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index 3d59c12a23..862317b969 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -840,7 +840,8 @@ def query_retrieve_comments_or_remarks(recID, display_order='od', display_since= cmt.body, cmt.status, cmt.nb_abuse_reports, - %(ranking)s cmt.id, + %(ranking)s cmt.title, + cmt.id, cmt.round_name, cmt.restriction, %(reply_to_column)s, @@ -851,7 +852,7 @@ def query_retrieve_comments_or_remarks(recID, display_order='od', display_since= %(ranking_only)s %(display_since)s ORDER BY %(display_order)s - """ % {'ranking' : ranking and ' cmt.nb_votes_yes, cmt.nb_votes_total, cmt.star_score, cmt.title, ' or '', + """ % {'ranking' : ranking and ' cmt.nb_votes_yes, cmt.nb_votes_total, cmt.star_score, ' or '', 'ranking_only' : ranking and ' AND cmt.star_score>0 ' or ' AND cmt.star_score=0 ', # 'id_bibrec' : recID > 0 and 'cmt.id_bibrec' or 'cmt.id_bibrec_or_bskEXTREC', # 'table' : recID > 0 and 'cmtRECORDCOMMENT' or 'bskRECORDCOMMENT', @@ -870,7 +871,7 @@ def query_retrieve_comments_or_remarks(recID, display_order='od', display_since= restriction = row[12] else: # when dealing with comments, row[8] holds restriction info: - restriction = row[8] + restriction = row[9] if user_info and check_user_can_view_comment(user_info, None, restriction)[0] != 0: # User cannot view comment. Look further continue @@ -1646,7 +1647,7 @@ def group_comments_by_round(comments, ranking=0): comment_rounds = {} ordered_comment_round_names = [] for comment in comments: - comment_round_name = ranking and comment[11] or comment[7] + comment_round_name = ranking and comment[11] or comment[8] if not comment_rounds.has_key(comment_round_name): comment_rounds[comment_round_name] = [] ordered_comment_round_names.append(comment_round_name) diff --git a/modules/webcomment/lib/webcomment_config.py b/modules/webcomment/lib/webcomment_config.py index c2c6ad3f19..52549fd8ab 100644 --- a/modules/webcomment/lib/webcomment_config.py +++ b/modules/webcomment/lib/webcomment_config.py @@ -24,6 +24,8 @@ __revision__ = "$Id$" from invenio.config import CFG_CERN_SITE +from invenio.search_engine_utils import get_fieldvalues +from invenio.webuser import collect_user_info CFG_WEBCOMMENT_ACTION_CODE = { 'ADD_COMMENT': 'C', @@ -33,20 +35,20 @@ } CFG_WEBCOMMENT_BODY_FORMATS = { - "HTML" : "HTML", - "TEXT" : "TXT", - "MARKDOWN" : "MD", + "HTML": "HTML", + "TEXT": "TXT", + "MARKDOWN": "MD", } CFG_WEBCOMMENT_OUTPUT_FORMATS = { - "HTML" : { - "WEB" : "WEB", - "EMAIL" : "HTML_EMAIL", - "CKEDITOR" : "CKEDITOR", + "HTML": { + "WEB": "WEB", + "EMAIL": "HTML_EMAIL", + "CKEDITOR": "CKEDITOR", }, - "TEXT" : { - "EMAIL" : "TEXT_EMAIL", - "TEXTAREA" : "TEXTAREA", + "TEXT": { + "EMAIL": "TEXT_EMAIL", + "TEXTAREA": "TEXTAREA", }, } @@ -54,13 +56,13 @@ # enforce comment submission deadlines. The configuration is composed of rules # (dictionary items). For a rule to be applied in the currently displayed # record, the dictionary key has to be in the list of values of the MARC field -# that is the first element of the tuple in that dictionary key's value. If that -# is the case, then the dealine is retrieved as the first value of the MARC -# field that is the second element of the tuple in that dictionary key's value. -# In order to programmatically check if the deadline has passed or not we need -# to know the format of the given deadline, using standard strftime conversion -# specifications <http://linux.die.net/man/3/strftime>. The deadline format is -# the third element of the tuple in that dictionary key's value. +# that is the first element of the tuple in that dictionary key's value. If +# that is the case, then the dealine is retrieved as the first value of the +# MARC field that is the second element of the tuple in that dictionary key's +# value. In order to programmatically check if the deadline has passed or not +# we need to know the format of the given deadline, using standard strftime +# conversion specifications <http://linux.die.net/man/3/strftime>. The deadline +# format is the third element of the tuple in that dictionary key's value. if CFG_CERN_SITE: CFG_WEBCOMMENT_DEADLINE_CONFIGURATION = { "ATLASPUBDRAFT": ( @@ -73,22 +75,81 @@ CFG_WEBCOMMENT_DEADLINE_CONFIGURATION = { } + +def check_user_is_editor(uid, record_id, data): + """Check if the user is editor. + + :param int uid: The user id + :param int record_id: The record id + :param dict data: Extra arguments + :return: If the user is editor + :rtype: bool + + .. note:: + + The report number is been splited and wrapped with proper suffix and + prefix for matching CERN's e-groups. + + """ + report_number_field = data.get('report_number_field') + report_number = get_fieldvalues(record_id, report_number_field) + + if report_number: + report_number = '-'.join(report_number[0].split('-')[1:-1]) + the_list = "{0}-{1}-{2}".format( + data.get('prefix'), report_number.lower(), data.get('suffix') + ) + user_info = collect_user_info(uid) + user_lists = user_info.get('group', []) + if the_list in user_lists: + return True + return False + +# Based on CFG_WEBCOMMENT_USER_EDITOR we can display an extra html element +# for users which are editors. The configuration uses the the collection name +# as a key which holds a tuple with two items. The first one is the MARC field +# which holds the collection and the seccond one is a dictionary. The +# dictionary *MUST* contain a key called `callback` which holds the check +# function. The check function *MUST* have `user_id` as first argument, the +# `record_id` as second and a third which contains any other data. +# Read more `~webcomment_config.check_user_is_editor` +if CFG_CERN_SITE: + CFG_WEBCOMMENT_EXTRA_CHECKBOX = { + "ATLAS": ( + "980__a", + dict( + report_number_field="037__a", + label="Post comment as Editor's response", + callback=check_user_is_editor, + prefix="atlas", + suffix="editor [cern]", + value="Editor response", + ) + ) + } +else: + CFG_WEBCOMMENT_EXTRA_CHECKBOX = {} + + # Exceptions: errors class InvenioWebCommentError(Exception): """A generic error for WebComment.""" def __init__(self, message): """Initialisation.""" self.message = message + def __str__(self): """String representation.""" return repr(self.message) + # Exceptions: warnings class InvenioWebCommentWarning(Exception): """A generic warning for WebComment.""" def __init__(self, message): """Initialisation.""" self.message = message + def __str__(self): """String representation.""" return repr(self.message) diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index 38e63aa193..78b444c9ff 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -28,9 +28,11 @@ import json from datetime import datetime, date -from invenio.webcomment_config import \ - CFG_WEBCOMMENT_BODY_FORMATS, \ - CFG_WEBCOMMENT_OUTPUT_FORMATS +from invenio.webcomment_config import ( + CFG_WEBCOMMENT_BODY_FORMATS, + CFG_WEBCOMMENT_OUTPUT_FORMATS, + CFG_WEBCOMMENT_EXTRA_CHECKBOX +) from invenio.urlutils import create_html_link, create_url from invenio.webuser import ( get_user_info, @@ -71,6 +73,7 @@ from invenio.search_engine_utils import get_fieldvalues from invenio.webcomment_config import CFG_WEBCOMMENT_DEADLINE_CONFIGURATION + class Template: """templating class, refer to webcomment.py for examples of call""" @@ -94,8 +97,8 @@ def tmpl_get_first_comments_without_ranking(self, recID, ln, comments, nb_commen c_body = 3 c_status = 4 c_nb_reports = 5 - c_id = 6 - c_body_format = 10 + c_id = 7 + c_body_format = 11 warnings = self.tmpl_warnings(warnings, ln) @@ -396,6 +399,7 @@ def tmpl_get_comment_without_ranking( body_format, status, nb_reports, + cmt_title, reply_link=None, report_link=None, undelete_link=None, @@ -516,7 +520,8 @@ def tmpl_get_comment_without_ranking( 'toggle_url': create_url(CFG_SITE_URL + '/' + CFG_SITE_RECORD + '/' + str(recID) + '/comments/toggle', {'comid': com_id, 'ln': ln, 'collapse': collapsed_p and '0' or '1', 'referer': user_info['uri']}), 'collapse_ctr_class': collapsed_p and 'webcomment_collapse_ctr_right' or 'webcomment_collapse_ctr_down', 'collapse_label': collapsed_p and _("Open") or _("Close")} - related_file_element = '' + + related_file_element = "" try: related_file = related_files[com_id] related_file_element = """ @@ -531,10 +536,15 @@ def tmpl_get_comment_without_ranking( except (TypeError, KeyError): pass + title_element = "" + if cmt_title: + title_element = """<div> %s </div>""" % (cmt_title) + out += """ <div class="webcomment_comment_box"> %(toggle_visibility_block)s <div class="webcomment_comment_avatar"><img class="webcomment_comment_avatar_default" src="%(site_url)s/img/user-icon-1-24x24.gif" alt="avatar" /></div> + %(title_element)s <div class="webcomment_comment_content"> <div class="webcomment_comment_title"> %(title)s @@ -553,18 +563,19 @@ def tmpl_get_comment_without_ranking( <div class="clearer"></div> </div> <div class="clearer"></div> -</div>""" % \ - {'title' : title, - 'body' : final_body, - 'links' : links, - 'attached_files_html': attached_files_html, - 'date': date_creation, - 'site_url': CFG_SITE_URL, - 'comid': com_id, - 'collapsible_content_style': collapsed_p and 'display:none' or '', - 'toggle_visibility_block': toggle_visibility_block, - 'related_file_element': related_file_element - } +</div>""" % { + 'title': title, + 'body': final_body, + 'links': links, + 'attached_files_html': attached_files_html, + 'date': date_creation, + 'site_url': CFG_SITE_URL, + 'comid': com_id, + 'collapsible_content_style': collapsed_p and 'display:none' or '', + 'toggle_visibility_block': toggle_visibility_block, + 'related_file_element': related_file_element, + 'title_element': title_element, + } return out def tmpl_get_comment_with_ranking( @@ -810,12 +821,13 @@ def tmpl_get_comments(self, req, recID, ln, c_body = 3 c_status = 4 c_nb_reports = 5 - c_id = 6 - c_round_name = 7 - c_restriction = 8 - reply_to = 9 - c_body_format = 10 - c_visibility = 11 + c_title = 6 + c_id = 7 + c_round_name = 8 + c_restriction = 9 + reply_to = 10 + c_body_format = 11 + c_visibility = 12 discussion = 'comments' comments_link = '<strong>%s (%i)</strong>' % (_('Comments'), total_nb_comments) reviews_link = '<a href="%s/%s/%s/reviews/">%s</a> (%i)' % (CFG_SITE_URL, CFG_SITE_RECORD, recID, _('Reviews'), total_nb_reviews) @@ -1008,6 +1020,7 @@ def tmpl_get_comments(self, req, recID, ln, comment[c_body_format], comment[c_status], comment[c_nb_reports], + comment[c_title], reply_link, report_link, undelete_link, @@ -1479,7 +1492,8 @@ def tmpl_add_comment_form( relate_to_file=None ): """ - Add form for comments + Add form for comments. + @param recID: record id @param uid: user id @param ln: language @@ -1488,21 +1502,36 @@ def tmpl_add_comment_form( @param textual_msg: same as 'msg', but contains the textual version in case user cannot display CKeditor @param warnings: list of warning tuples (warning_text, warning_color) - @param can_attach_files: if user can upload attach file to record or not - @param user_is_subscribed_to_discussion: True if user already receives new comments by email - @param reply_to: the ID of the comment we are replying to. None if not replying + @param can_attach_files: if user can upload attach file to record or + not + @param user_is_subscribed_to_discussion: True if user already receives + new comments by email + @param reply_to: the ID of the comment we are replying to. None if + not replying @return html add comment form """ _ = gettext_set_language(ln) - link_dic = { 'siteurl' : CFG_SITE_URL, - 'CFG_SITE_RECORD' : CFG_SITE_RECORD, - 'module' : 'comments', - 'function' : 'add', - 'arguments' : 'ln=%s&amp;action=%s' % (ln, 'SUBMIT'), - 'recID' : recID} + link_dic = { + 'siteurl': CFG_SITE_URL, + 'CFG_SITE_RECORD': CFG_SITE_RECORD, + 'module': 'comments', + 'function': 'add', + 'arguments': 'ln=%s&amp;action=%s' % (ln, 'SUBMIT'), + 'recID': recID + } if textual_msg is None: textual_msg = msg + # check if is editor + has_extra_checkbox = () + for (matching_value, (matching_key, data)) in CFG_WEBCOMMENT_EXTRA_CHECKBOX.items(): + if matching_value in get_fieldvalues(recID, matching_key): + callback = data.get("callback") + extra_checkbox = callback(uid, recID, data) + if extra_checkbox: + has_extra_checkbox = extra_checkbox, data.get('label'), data.get('value') + break + # FIXME a cleaner handling of nicknames is needed. if not nickname: (uid, nickname, display) = get_user_info(uid) @@ -1518,7 +1547,7 @@ def tmpl_add_comment_form( if not CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR: if CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING: - note += '<br />' + '&nbsp;'*10 + cgi.escape('You can use Markdown syntax to write your comment.') + note += '<br />' + '&nbsp;'*10 + cgi.escape('You can use Markdown syntax to write your comment.') else: # NOTE: Currently we escape all HTML tags before displaying plain text. Should we go back to this approach? # To go back to this approach we probably simply have to run email_quoted_text2html with wash_p=True @@ -1555,27 +1584,40 @@ def tmpl_add_comment_form( _("Max %i files") % CFG_WEBCOMMENT_MAX_ATTACHED_FILES, 'file_size_limit_msg': CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE > 0 and _("Max %(x_nb_bytes)s per file") % {'x_nb_bytes': (CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE < 1024*1024 and (str(CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE/1024) + 'KB') or (str(CFG_WEBCOMMENT_MAX_ATTACHMENT_SIZE/(1024*1024)) + 'MB'))} or ''} + # Check if the user is editor, CFG_WEBCOMMENT_USER_EDITOR should + # be configured first + extra_checkbox = "" + if has_extra_checkbox and len(has_extra_checkbox) == 3: + extra_checkbox = ( + "<input type='checkbox' name='extra_checkbox' value='{0}' />" + "{1}<br />").format( + has_extra_checkbox[2], has_extra_checkbox[1]) + if CFG_CERN_SITE: - editor = get_html_text_editor(name='msg', - content=msg, - textual_content=textual_msg, - width='100%', - height='400px', - enabled=CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, - file_upload_url=file_upload_url, - toolbar_set = "WebComment", - custom_configurations_path='/ckeditor/cds-ckeditor-config.js', - ln=ln) + editor = get_html_text_editor( + name='msg', + content=msg, + textual_content=textual_msg, + width='100%', + height='400px', + enabled=CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, + file_upload_url=file_upload_url, + toolbar_set="WebComment", + custom_configurations_path='/ckeditor/cds-ckeditor-config.js', + ln=ln + ) else: - editor = get_html_text_editor(name='msg', - content=msg, - textual_content=textual_msg, - width='100%', - height='400px', - enabled=CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, - file_upload_url=file_upload_url, - toolbar_set = "WebComment", - ln=ln) + editor = get_html_text_editor( + name='msg', + content=msg, + textual_content=textual_msg, + width='100%', + height='400px', + enabled=CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR, + file_upload_url=file_upload_url, + toolbar_set="WebComment", + ln=ln + ) subscribe_to_discussion = '' if not user_is_subscribed_to_discussion: @@ -1599,17 +1641,18 @@ def tmpl_add_comment_form( "{1}</div>").format(relate_file_title, relate_file_selector) form = """<div id="comment-write"><h2>%(add_comment)s</h2> - %(editor)s + %(editor)s %(relate_file_element)s + %(extra_checkbox)s %(simple_attach_file_interface)s - <div class="submit-area"> + <div class="submit-area"> <br /> <span class="reportabuse">%(note)s</span> <br /> <br /> %(subscribe_to_discussion)s<br /><br /> <input class="adminbutton" type="submit" value="Add comment" onclick="user_must_confirm_before_leaving_page = false;return true;"/> %(reply_to)s + </div> </div> - </div> """ % {'note': note, 'record_label': _("Article") + ":", 'comment_label': _("Comment") + ":", @@ -1618,7 +1661,8 @@ def tmpl_add_comment_form( 'subscribe_to_discussion': subscribe_to_discussion, 'reply_to': reply_to and '<input type="hidden" name="comid" value="%s"/>' % reply_to or '', 'simple_attach_file_interface': simple_attach_file_interface, - 'relate_file_element': relate_file_element + 'relate_file_element': relate_file_element, + 'extra_checkbox': extra_checkbox, } form_link = "%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/%(function)s?%(arguments)s" % link_dic form = self.create_write_comment_hiddenform(action=form_link, method="post", text=form, button='Add comment', diff --git a/modules/webcomment/lib/webcomment_webinterface.py b/modules/webcomment/lib/webcomment_webinterface.py index 2601002a04..e1cf197de1 100644 --- a/modules/webcomment/lib/webcomment_webinterface.py +++ b/modules/webcomment/lib/webcomment_webinterface.py @@ -19,6 +19,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ Comments and reviews for records: web interface """ +from docutils.nodes import note __lastupdated__ = """$Date$""" @@ -316,16 +317,21 @@ def add(self, req, form): this discussion @return the full html page. """ - argd = wash_urlargd(form, {'action': (str, "DISPLAY"), - 'msg': (str, ""), - 'note': (str, ''), - 'score': (int, 0), - 'comid': (int, 0), - 'editor_type': (str, ""), - 'subscribe': (str, ""), - 'cookie': (str, ""), - 'relate_file': (str, "") - }) + argd = wash_urlargd( + form, + { + 'action': (str, "DISPLAY"), + 'msg': (str, ""), + 'note': (str, ''), + 'score': (int, 0), + 'comid': (int, 0), + 'editor_type': (str, ""), + 'subscribe': (str, ""), + 'cookie': (str, ""), + 'relate_file': (str, ""), + 'extra_checkbox': (str, ""), + } + ) _ = gettext_set_language(argd['ln']) actions = ['DISPLAY', 'REPLY', 'SUBMIT'] @@ -506,13 +512,17 @@ def add(self, req, form): # User is not already subscribed, and asked to subscribe subscribe = True + note = argd['note'] + if argd['extra_checkbox']: + note = argd['extra_checkbox'] + body = perform_request_add_comment_or_remark( recID=self.recid, ln=argd['ln'], uid=uid, action=argd['action'], msg=argd['msg'], - note=argd['note'], + note=note, score=argd['score'], reviews=self.discussion, comID=argd['comid'], From 6b39a7c1ca548ad048a6a7c5f78084aa548562b3 Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis <me@drjova.com> Date: Wed, 18 Mar 2015 11:50:38 +0100 Subject: [PATCH 45/83] WebComment: fix collapse and review issues * FIX issue on reviews which caused `NameError`. * Disables filtering on reviews. * FIX issues with collapsed comment which didn't save user's choices. Signed-off-by: Harris Tzovanakis <me@drjova.com> --- modules/webcomment/lib/webcomment.py | 4 ++-- modules/webcomment/lib/webcomment_templates.py | 18 ++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index 862317b969..ad04370721 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # This file is part of Invenio. -# Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 CERN. +# Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -266,7 +266,7 @@ def perform_request_display_comments_or_remarks( if reviews: res = [row[:] + (row[10] in user_collapsed_comments,) for row in res] else: - res = [row[:] + (row[6] in user_collapsed_comments,) for row in res] + res = [row[:] + (row[7] in user_collapsed_comments,) for row in res] # Send to template avg_score = 0.0 diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index 78b444c9ff..546faa0354 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # This file is part of Invenio. -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 CERN. +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -686,20 +686,6 @@ def tmpl_get_comment_with_ranking( _body = '<div class="webcomment_review_pending_approval_message">This review is pending approval due to user reports.</div>' links = '' related_file_element = '' - try: - related_file = related_files[com_id] - related_file_element = """ - <div class="cmt_file_relation" doc_code="%(id_bibdoc)s:%(version)s" style="float:right"> - This comment is related with file %(docname)s, version %(version)s - </div> - """ % { - 'docname': related_file['docname'], - 'version': related_file['version'], - 'id_bibdoc': related_file['id_bibdoc'] - } - except (TypeError, KeyError): - pass - out += ''' <div class="webcomment_review_box"> @@ -1278,7 +1264,7 @@ def tmpl_get_comments(self, req, recID, ln, filter_file=filter_for_file, page=page, nb_per_page=nb_per_page, - nb_pages=nb_pages), + nb_pages=nb_pages) if not reviews else '', 'deadline_text': deadline_text, } From fbba3f6374628161d30606dd7b0b082352f11791 Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis <me@drjova.com> Date: Tue, 17 Mar 2015 11:25:56 +0100 Subject: [PATCH 46/83] WebComment: `collapse` and `expand` all comments * IMPROVEMENT `collapse` and `expand` all comments button. Signed-off-by: Harris Tzovanakis <me@drjova.com> --- modules/webcomment/lib/webcomment.py | 46 +++--- .../webcomment/lib/webcomment_templates.py | 135 +++++++++++++----- .../webcomment/lib/webcomment_webinterface.py | 8 +- 3 files changed, 131 insertions(+), 58 deletions(-) diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index ad04370721..b7a96e144e 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -2336,7 +2336,7 @@ def check_user_can_attach_file_to_comments(user_info, recid): record_primary_collection = guess_primary_collection_of_a_record(recid) return acc_authorize_action(user_info, 'attachcommentfile', authorized_if_no_roles=False, collection=record_primary_collection) -def toggle_comment_visibility(uid, comid, collapse, recid): +def toggle_comment_visibility(uid, comid, collapse, recid, force=False): """ Toggle the visibility of the given comment (collapse) for the given user. Return the new visibility @@ -2345,6 +2345,7 @@ def toggle_comment_visibility(uid, comid, collapse, recid): @param comid: the comment id to close/open @param collapse: if the comment is to be closed (1) or opened (0) @param recid: the record id to which the comment belongs + @param force: if we need to delete previous comment state @return: if the comment is visible or not after the update """ # We rely on the client to tell if comment should be collapsed or @@ -2360,24 +2361,33 @@ def toggle_comment_visibility(uid, comid, collapse, recid): # when deleting an entry, as in the worst case no line would be # removed. For optimized retrieval of row to delete, the id_bibrec # column is used, though not strictly necessary. - if collapse: - query = """SELECT id_bibrec from cmtRECORDCOMMENT WHERE id=%s""" - params = (comid,) - res = run_sql(query, params) - if res: - query = """INSERT IGNORE INTO cmtCOLLAPSED (id_bibrec, id_cmtRECORDCOMMENT, id_user) - VALUES (%s, %s, %s)""" - params = (res[0][0], comid, uid) + + # Split all comment ids + splited_comment_ids = comid.split(',') + # `False` if the comment is hidden else `True` if the comment is visible + comment_state = False + if collapse and force or not collapse: + for comment_id in splited_comment_ids: + query = """DELETE FROM cmtCOLLAPSED WHERE + id_cmtRECORDCOMMENT=%s and + id_user=%s and + id_bibrec=%s""" + params = (comment_id, uid, recid) run_sql(query, params) - return True - else: - query = """DELETE FROM cmtCOLLAPSED WHERE - id_cmtRECORDCOMMENT=%s and - id_user=%s and - id_bibrec=%s""" - params = (comid, uid, recid) - run_sql(query, params) - return False + + if collapse: + for comment_id in splited_comment_ids: + query = """SELECT id_bibrec from cmtRECORDCOMMENT WHERE id=%s""" + params = (comment_id,) + res = run_sql(query, params) + if res: + query = """INSERT IGNORE INTO cmtCOLLAPSED (id_bibrec, id_cmtRECORDCOMMENT, id_user) + VALUES (%s, %s, %s)""" + params = (res[0][0], comment_id, uid) + run_sql(query, params) + comment_state = True + return comment_state + def get_user_collapsed_comments_for_record(uid, recid): """ diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index 546faa0354..401ab19a08 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -896,45 +896,106 @@ def tmpl_get_comments(self, req, recID, ln, comments_rows += _('%(x_nb)i comments for round "%(x_name)s"') % {'x_nb': len(comments_list), 'x_name': comment_round_name}+ "</a><br/>" comments_rows += '<div id="cmtSubRound%s" class="cmtsubround" style="%s">' % (comment_round_name, comment_round_style) - comments_rows += ''' - <script type='text/javascript'>//<![CDATA[ - function toggle_visibility(this_link, comid, duration) { - if (duration == null) duration = 0; - var isVisible = $('#collapsible_content_' + comid).is(':visible'); - $('#collapsible_content_' + comid).toggle(duration); - $('#collapsible_ctr_' + comid).toggleClass('webcomment_collapse_ctr_down'); - $('#collapsible_ctr_' + comid).toggleClass('webcomment_collapse_ctr_right'); - if (isVisible){ - $('#collapsible_ctr_' + comid).attr('title', '%(open_label)s'); - $('#collapsible_ctr_' + comid + ' > span').html('%(open_label)s'); - } else { - $('#collapsible_ctr_' + comid).attr('title', '%(close_label)s'); - $('#collapsible_ctr_' + comid + ' > span').html('%(close_label)s'); - } - $.ajax({ - type: 'POST', - url: '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/toggle', - data: {'comid': comid, 'ln': '%(ln)s', 'collapse': isVisible && 1 || 0} + if not reviews and not isGuestUser(uid): + comments_rows += ''' + <a href="javascript:void(0)" data-toggle="collapse" data-collapse>%(collapse_text)s</a> | <a href="javacript:void(0)" data-toggle="uncollapse" data-collapse>%(expand_text)s</a> + <script type="text/javascript"> + $(document).ready(function(){ + $('[data-collapse]').on('click', function(){ + var $that = $(this); + var collapse = $that.data('toggle'); + $.when( + find_opposite_ids(collapse) + ).done(function(ids){ + toggle_visibility($(this), ids, 0, collapse, 1); + }); + }); + /* If type == collapse we need to find only + * the expanded comment and the vice versa. + */ + function find_opposite_ids(type){ + var $prom = $.Deferred(); + var ids = []; + $.when( + $('.webcomment_toggle_visibility > a').each(function(){ + var that = $(this); + var comid = that.attr('id').split('_')[2]; + var isVisible = $('#collapsible_content_' + comid).is(':visible'); + if(type == "collapse"){ + if(isVisible){ + ids.push(comid); + } + }else{ + if(!isVisible){ + ids.push(comid); + } + } + }) + ).done(function(){ + return $prom.resolve(ids); + }) + return $prom.promise(); + } + }); + </script> + <script type='text/javascript'>//<![CDATA[ + function toggle_visibility(this_link, comid, duration, type, force) { + force = (force === undefined ) ? 0 : 1; + type = (type === undefined) ? 0 : type; + duration = (duration === undefined) ? 0 : duration; + if(force){ + $.each(comid, function(index, id){ + var isVisible = $('#collapsible_content_' + id).is(':visible'); + toggle_divs(id, duration, isVisible); + }); + comid = comid.join(','); + var isVisible = (type == "collapse") ? 1 : 0; + }else{ + var isVisible = $('#collapsible_content_' + comid).is(':visible'); + toggle_divs(comid, duration, isVisible); + } + $.ajax({ + type: 'POST', + url: '%(siteurl)s/%(CFG_SITE_RECORD)s/%(recID)s/comments/toggle', + data: {'comid': comid, 'ln': '%(ln)s', 'collapse': isVisible && 1 || 0, 'force': force} }); - /* Replace our link with a jump to the adequate, in case needed - (default link is for non-Javascript user) */ - this_link.href = "#C" + comid - /* Find out if after closing comment we shall scroll a bit to the top, - i.e. go back to main anchor of the comment that we have just set */ - var top = $(window).scrollTop(); - if ($(window).scrollTop() >= $("#C" + comid).offset().top) { - // Our comment is now above the window: scroll to it - return true; + /* Replace our link with a jump to the adequate, in case needed + (default link is for non-Javascript user) */ + if(!force){ + this_link.href = "#C" + comid + /* Find out if after closing comment we shall scroll a bit to the top, + i.e. go back to main anchor of the comment that we have just set */ + var top = $(window).scrollTop(); + if ($(window).scrollTop() >= $("#C" + comid).offset().top) { + // Our comment is now above the window: scroll to it + return true; + } + return false; + } } - return false; - } - //]]></script> - ''' % {'siteurl': CFG_SITE_URL, - 'recID': recID, - 'ln': ln, - 'CFG_SITE_RECORD': CFG_SITE_RECORD, - 'open_label': _("Open"), - 'close_label': _("Close")} + function toggle_divs(comid, duration, isVisible){ + $('#collapsible_content_' + comid).toggle(duration); + $('#collapsible_ctr_' + comid).toggleClass('webcomment_collapse_ctr_down'); + $('#collapsible_ctr_' + comid).toggleClass('webcomment_collapse_ctr_right'); + if (isVisible){ + $('#collapsible_ctr_' + comid).attr('title', '%(open_label)s'); + $('#collapsible_ctr_' + comid + ' > span').html('%(open_label)s'); + } else { + $('#collapsible_ctr_' + comid).attr('title', '%(close_label)s'); + $('#collapsible_ctr_' + comid + ' > span').html('%(close_label)s'); + } + } + //]]></script> + ''' % {'siteurl': CFG_SITE_URL, + 'recID': recID, + 'ln': ln, + 'CFG_SITE_RECORD': CFG_SITE_RECORD, + 'open_label': _("Open"), + 'close_label': _("Close"), + 'expand_text': _("Expand all"), + 'collapse_text': _("Collapse all"), + 'error_expand_or_collapse_all': _("An error occured please try again later") + } thread_history = [0] previous_depth = 0 for comment in comments_list: diff --git a/modules/webcomment/lib/webcomment_webinterface.py b/modules/webcomment/lib/webcomment_webinterface.py index e1cf197de1..e90434a40b 100644 --- a/modules/webcomment/lib/webcomment_webinterface.py +++ b/modules/webcomment/lib/webcomment_webinterface.py @@ -770,9 +770,10 @@ def toggle(self, req, form): """ Store the visibility of a comment for current user """ - argd = wash_urlargd(form, {'comid': (int, -1), + argd = wash_urlargd(form, {'comid': (str, ""), 'referer': (str, None), - 'collapse': (int, 1)}) + 'collapse': (int, 1), + 'force': (int, 0)}) uid = getUid(req) @@ -780,11 +781,12 @@ def toggle(self, req, form): # We do not store information for guests return '' - toggle_comment_visibility(uid, argd['comid'], argd['collapse'], self.recid) + toggle_comment_visibility(uid, argd['comid'], argd['collapse'], self.recid, argd.get('force')) if argd['referer']: return redirect_to_url(req, CFG_SITE_SECURE_URL + \ (not argd['referer'].startswith('/') and '/' or '') + \ argd['referer'] + '#' + str(argd['comid'])) + return "ok" class WebInterfaceCommentsFiles(WebInterfaceDirectory): """Handle <strike>upload and </strike> access to files for comments. From 6e8398194cbb2536bb8133d5eacc81ee10182c2c Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis <me@drjova.com> Date: Fri, 20 Mar 2015 10:14:08 +0100 Subject: [PATCH 47/83] WebComment: cosmetic updates * FIX Remove `<br />` elements which were caused overlap in the interface. * IMPROVEMENT Cleaner comment interface. Signed-off-by: Harris Tzovanakis <me@drjova.com> --- .../webcomment/lib/webcomment_templates.py | 145 +++++++------ modules/webstyle/css/invenio.css | 199 ++++++++++++------ 2 files changed, 221 insertions(+), 123 deletions(-) diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index 401ab19a08..5efa9f4de5 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -538,17 +538,17 @@ def tmpl_get_comment_without_ranking( title_element = "" if cmt_title: - title_element = """<div> %s </div>""" % (cmt_title) + title_element = "<span class='extra-info'>{0}</span>".format( + cmt_title) out += """ <div class="webcomment_comment_box"> %(toggle_visibility_block)s <div class="webcomment_comment_avatar"><img class="webcomment_comment_avatar_default" src="%(site_url)s/img/user-icon-1-24x24.gif" alt="avatar" /></div> - %(title_element)s <div class="webcomment_comment_content"> <div class="webcomment_comment_title"> %(title)s - <div class="webcomment_comment_date">%(date)s</div> + <div class="webcomment_comment_date">%(date)s</div> %(title_element)s <a class="webcomment_permalink" title="Permalink to this comment" href="#C%(comid)s">¶</a> %(related_file_element)s </div> @@ -861,6 +861,7 @@ def tmpl_get_comments(self, req, recID, ln, req = None ## comments table comments_rows = '' + toggle_script = '' last_comment_round_name = None comment_round_names = [comment[0] for comment in comments] if comment_round_names: @@ -897,8 +898,7 @@ def tmpl_get_comments(self, req, recID, ln, comments_rows += '<div id="cmtSubRound%s" class="cmtsubround" style="%s">' % (comment_round_name, comment_round_style) if not reviews and not isGuestUser(uid): - comments_rows += ''' - <a href="javascript:void(0)" data-toggle="collapse" data-collapse>%(collapse_text)s</a> | <a href="javacript:void(0)" data-toggle="uncollapse" data-collapse>%(expand_text)s</a> + toggle_script = ''' <script type="text/javascript"> $(document).ready(function(){ $('[data-collapse]').on('click', function(){ @@ -986,6 +986,13 @@ def tmpl_get_comments(self, req, recID, ln, } } //]]></script> + <div id="action-area"> + <ul> + <li><a href="javascript:void(0)" data-toggle="collapse" data-collapse>%(collapse_text)s</a></li> + <li>|</li> + <li> <a href="javascript:void(0)" data-toggle="uncollapse" data-collapse>%(expand_text)s</a></li> + </ul> + </div> ''' % {'siteurl': CFG_SITE_URL, 'recID': recID, 'ln': ln, @@ -1223,7 +1230,7 @@ def tmpl_get_comments(self, req, recID, ln, link_label=_('Subscribe')) + \ '</strong>' + \ ' to this discussion. You will then receive all new comments by email.' + \ - '</p>' + '</p> <div class="clear"></div> ' elif user_can_unsubscribe_from_discussion: subscription_link = \ '<p class="comment-subscribe">' + \ @@ -1241,52 +1248,62 @@ def tmpl_get_comments(self, req, recID, ln, link_label=_('Unsubscribe')) + \ '</strong>' + \ ' from this discussion. You will no longer receive emails about new comments.' + \ - '</p>' + '</p> <div class="clear"></div>' deadline_text = "" - for (matching_value, (matching_key, deadline_key, deadline_value_format)) in CFG_WEBCOMMENT_DEADLINE_CONFIGURATION.items(): - if matching_value in get_fieldvalues(recID, matching_key): - deadline_value = get_fieldvalues(recID, deadline_key) - if deadline_value: - try: - deadline_object = datetime.strptime(deadline_value[0], deadline_value_format) - except ValueError: - break - datetime_left = deadline_object - datetime.now() - # People can still submit comments on the day of the - # deadline, so make sure to add a day here. - days_left = datetime_left.days + 1 - deadline_struct_time = date.timetuple(deadline_object) - deadline_dategui = convert_datestruct_to_dategui(deadline_struct_time, ln).split(",")[0] - if days_left > 0: - deadline_text = _("Please submit your comments within %i days, by %s." % (days_left, deadline_dategui)) - deadline_text = "<span class=\"warninggreen\">" + deadline_text + "</span><br />" - elif days_left == 0: - deadline_text = _("Please submit your comments by today.") - deadline_text = "<span class=\"warninggreen\">" + deadline_text + "</span><br />" - else: - deadline_text = _("The deadline to submit comments expired %i days ago, on %s." % (abs(days_left), deadline_dategui)) - deadline_text = "<span class=\"warningred\">" + deadline_text + "</span><br />" - # If at least one matching value is found, then there is no - # need to look for more. - break + if not reviews: + for (matching_value, (matching_key, deadline_key, deadline_value_format)) in CFG_WEBCOMMENT_DEADLINE_CONFIGURATION.items(): + if matching_value in get_fieldvalues(recID, matching_key): + deadline_value = get_fieldvalues(recID, deadline_key) + if deadline_value: + try: + deadline_object = datetime.strptime(deadline_value[0], deadline_value_format) + except ValueError: + break + datetime_left = deadline_object - datetime.now() + # People can still submit comments on the day of the + # deadline, so make sure to add a day here. + days_left = datetime_left.days + 1 + deadline_struct_time = date.timetuple(deadline_object) + deadline_dategui = convert_datestruct_to_dategui(deadline_struct_time, ln).split(",")[0] + if days_left > 0: + deadline_text = _("Please submit your comments within %i days, by %s." % (days_left, deadline_dategui)) + deadline_text = "<span class=\"deadline-warnings deadline-warnings-green\">" + deadline_text + "</span>" + elif days_left == 0: + deadline_text = _("Please submit your comments by today.") + deadline_text = "<span class=\"deadline-warnings deadline-warnings-green\">" + deadline_text + "</span>" + else: + deadline_text = _("The deadline to submit comments expired %i days ago, on %s." % (abs(days_left), deadline_dategui)) + deadline_text = "<span class=\"deadline-warnings deadline-warnings-red \">" + deadline_text + "</span>" + # If at least one matching value is found, then there is no + # need to look for more. + break # do NOT remove the HTML comments below. Used for parsing body = ''' -%(comments_and_review_tabs)s -%(subscription_link_before)s -%(deadline_text)s -%(filtering_script)s -<div style="clear:both"></div> -<br /> -<!-- start comments table --> -<div class="webcomment_comment_table"> - %(comments_rows)s -</div> -<!-- end comments table --> -%(review_or_comment_first)s -%(subscription_link_after)s -''' % { + %(comments_and_review_tabs)s + %(subscription_link_before)s + <div class="clear"></div> + <div id="deadline-wrapper"> + %(deadline_text)s + </div> + <div class="clear"></div> + <div id="tools-and-filtering-wrapper"> + %(toggle_script)s + %(filtering_script)s + </div> + <div class="clear"></div> + <!-- start comments table --> + <div class="webcomment_comment_table"> + %(comments_rows)s + </div> + <!-- end comments table --> + <div class="clear"></div> + %(review_or_comment_first)s + <div class="clear"></div> + %(subscription_link_after)s + <div class="clear"></div> + ''' % { 'record_label': _("Record"), 'back_label': _("Back to search results"), 'total_label': total_label, @@ -1327,6 +1344,7 @@ def tmpl_get_comments(self, req, recID, ln, nb_per_page=nb_per_page, nb_pages=nb_pages) if not reviews else '', 'deadline_text': deadline_text, + 'toggle_script': toggle_script } # form is not currently used. reserved for an eventual purpose @@ -1590,11 +1608,11 @@ def tmpl_add_comment_form( note = _("Note: you have not %(x_url_open)sdefined your nickname%(x_url_close)s. %(x_nickname)s will be displayed as the author of this comment.") % \ {'x_url_open': link, 'x_url_close': '</a>', - 'x_nickname': ' <br /><i>' + display + '</i>'} - + 'x_nickname': ' <i>' + display + '</i>'} + note = "<p>{0}</p>".format(note) if not CFG_WEBCOMMENT_USE_RICH_TEXT_EDITOR: if CFG_WEBCOMMENT_ENABLE_MARKDOWN_TEXT_RENDERING: - note += '<br />' + '&nbsp;'*10 + cgi.escape('You can use Markdown syntax to write your comment.') + note += "<p>{0}</p>".format(_("You can use Markdown syntax to write your comment.")) else: # NOTE: Currently we escape all HTML tags before displaying plain text. Should we go back to this approach? # To go back to this approach we probably simply have to run email_quoted_text2html with wash_p=True @@ -1610,15 +1628,15 @@ def tmpl_add_comment_form( file_upload_url = None simple_attach_file_interface = '' if isGuestUser(uid): - simple_attach_file_interface = "<small><em>%s</em></small><br/>" % _("Once logged in, authorized users can also attach files.") + simple_attach_file_interface = "<label><em>%s</em></label>" % _("Once logged in, authorized users can also attach files.") if can_attach_files: # Note that files can be uploaded only when user is logged in #file_upload_url = '%s/%s/%i/comments/attachments/put' % \ # (CFG_SITE_URL, CFG_SITE_RECORD, recID) simple_attach_file_interface = ''' <div id="uploadcommentattachmentsinterface"> - <small>%(attach_msg)s: <em>(%(nb_files_limit_msg)s. %(file_size_limit_msg)s)</em></small><br /> - <input class="multi max-%(CFG_WEBCOMMENT_MAX_ATTACHED_FILES)s" type="file" name="commentattachment[]"/><br /> + <label>%(attach_msg)s: <small><em>(%(nb_files_limit_msg)s. %(file_size_limit_msg)s)</em></small></label><br /> + <input class="multi max-%(CFG_WEBCOMMENT_MAX_ATTACHED_FILES)s" type="file" name="commentattachment[]"/> <noscript> <input type="file" name="commentattachment[]" /><br /> </noscript> @@ -1669,7 +1687,7 @@ def tmpl_add_comment_form( subscribe_to_discussion = '' if not user_is_subscribed_to_discussion: # Offer to subscribe to discussion - subscribe_to_discussion = '<small><input type="checkbox" name="subscribe" id="subscribe"/><label for="subscribe">%s</label></small>' % _("Send me an email when a new comment is posted") + subscribe_to_discussion = '<label for="subscribe"><input type="checkbox" name="subscribe" id="subscribe"/> %s</label>' % _("Send me an email when a new comment is posted") relate_file_element = "" relate_file_selector = self.tmpl_bibdocfile_selector_element( @@ -1684,18 +1702,17 @@ def tmpl_add_comment_form( "Relate this comment to an existing file revision" ) relate_file_element = ( - "<div id='relate_file'><small>{0}</small>" - "{1}</div>").format(relate_file_title, relate_file_selector) + "<label>{0}</label><br />{1}").format( + relate_file_title, relate_file_selector) form = """<div id="comment-write"><h2>%(add_comment)s</h2> %(editor)s - %(relate_file_element)s - %(extra_checkbox)s - %(simple_attach_file_interface)s + <div class="comment-submit-area-note">%(note)s</div> + <p class='comment-submit-area-option'>%(relate_file_element)s</p> + <p class='comment-submit-area-option'>%(simple_attach_file_interface)s</p> + <p class='comment-submit-area-option'>%(extra_checkbox)s</p> + <p class='comment-submit-area-option'>%(subscribe_to_discussion)s</p> <div class="submit-area"> - <br /> - <span class="reportabuse">%(note)s</span> <br /> <br /> - %(subscribe_to_discussion)s<br /><br /> <input class="adminbutton" type="submit" value="Add comment" onclick="user_must_confirm_before_leaving_page = false;return true;"/> %(reply_to)s </div> @@ -3306,7 +3323,7 @@ def tmpl_comment_filtering_box_and_script( var matching_comments_number = $("#matching_comments_number"); var filter_input = $("#filter_input"); - var filter_area = $("#filter_area"); + var filter_area = $("#filter-area"); var no_matches_class = 'filter-no-matches'; var selectize_input = $('#selectize_input'); @@ -3459,7 +3476,7 @@ def tmpl_comment_filtering_box_and_script( }); </script> - <div id="filter_area"> + <div id="filter-area"> <p class="filter-area-count"><label id="matching_comments_number"></p> <input id="filter_input" placeholder="%(filter_placeholder)s" value="" /> %(select_element_with_files)s diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index e04a4b57f6..a102283973 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -1229,7 +1229,6 @@ form input[disabled], form input[disabled="true"]{ #comment-write { border:1px solid #ccc; margin-top:30px; - margin-right:50px; padding:25px 20px 5px ; /* 25 20 20 */ position:relative; clear:right; @@ -1246,19 +1245,20 @@ form input[disabled], form input[disabled="true"]{ } #comment-write .submit-area { - margin: 0px 0 0 -5px; + margin: 0px; } /* Subscribe to comment*/ .comment-subscribe { - /*color:#f00;*/ - font-size:small; - overflow:hidden; - padding:7px 0 5px 5px; - position:relative; - width:95%; - background:#dfe6f2; - /*border:1px solid #ccc;*/ + background: #d9edf7; + width: 98%; + text-align: left; + display: inline-block; + font-size: 14px; + margin: 20px 0; + padding: 10px 1%; + color: #31708f; + border: 1px solid #c4ebf3; } .warninggreen { @@ -1963,9 +1963,7 @@ ul.bsk_export_as_list{ dd{ margin-bottom: 10px; } -.cmtsubround { - margin: 5px 15px 5px; -} + .cmtfilesblock{ background-color:#f5f5f5; border-top:1px solid #eee; @@ -3562,8 +3560,8 @@ a#advbox-toggle{ .webcomment_comment_table { border: 0px solid black; - width: 95%; - margin:10px; + width: 100%; + margin:0px; font-size:small; } @@ -4054,57 +4052,62 @@ a.author_orcid_image_link { } /* WebComment filtering and related files */ -#filter_area{ - float: right; - margin-top: -5px; - height: 20px; - display: inline-table; - text-align: right; +#tools-and-filtering-wrapper{ + margin: 0px; + padding-bottom: 20px; + display: inline-block; + width: 100%; +} +#filter-area{ + text-align: right; + float: right; + width: 50%; } -#filter_area input{ - padding: 4px; - margin: 0; - width: 240px; - font-size: 12px; +#filter-area input{ + padding: 4px; + margin: 0; + width: 240px; + font-size: 12px; } -#filter_area p{ - display: inline; - padding: 0; - margin: 0; +#filter-area p{ + display: inline; + padding: 0; + margin: 0; } -#filter_area #matching_comments_number.filter-no-matches { - color: #b85757; +#filter-area #matching_comments_number.filter-no-matches { + color: #b85757; } -#filter_area p.filter-area-count { - color: #777; - font-size: 13px; - padding-right: 10px; +#filter-area p.filter-area-count { + color: #777; + font-size: 13px; + padding-right: 10px; } -#filter_area #selectize_input { - width: 252px; +#filter-area #selectize_input { + width: 252px; } -#filter_area .filter-area-advance { - margin-top: 5px; +#filter-area .filter-area-advance { + margin-top: 5px; } -#filter_area .filter-area-advance a.toggle-advance-search { - font-size: 12px; - line-height: 20px; - vertical-align: top; - background-image: url('/img/filter_filled.png'); - background-position: 0 2px; - background-repeat: no-repeat; - padding-left: 12px; +#filter-area .filter-area-advance a.toggle-advance-search { + font-size: 12px; + line-height: 20px; + vertical-align: top; + background-image: url('/img/filter_filled.png'); + background-position: 0 2px; + background-repeat: no-repeat; + padding-left: 12px; } -#cmtRound.cmtround { - margin-top: 80px; +#cmtRound{ + padding-bottom: 20px; + border-bottom: 1px solid #ccc; } #cmtSubRound .cmt_file_relation { @@ -4134,18 +4137,96 @@ a.author_orcid_image_link { } .webcomments-matches-alert { - text-align: center; - font-size: 14px; - margin-bottom: 20px; - color: #8a6d3b; - background: #fcf8e3; - border: 1px solid #ded18a; - padding: 10px 5px; + text-align: center; + font-size: 14px; + margin: 20px 0; + color: #8a6d3b; + background: #fcf8e3; + border: 1px solid #ded18a; + padding: 10px 5px; } #search_next_page { - text-align: right!important; - margin-top: 20px; + text-align: right!important; + margin-top: 20px; +} + +#deadline-wrapper{ + width: 100%; + display: inline-block; +} + +.deadline-warnings{ + width: 98%; + text-align: left; + display: inline-block; + font-size: 14px; + margin-bottom: 20px; + display: inline-block; + padding: 10px 1%; +} + +.deadline-warnings.deadline-warnings-red{ + color: #a94442; + background: #f2dede; + border: 1px solid #ebccd1; +} + +.deadline-warnings.deadline-warnings-green{ + color: #3c763d; + background: #dff0d8; + border: 1px solid #d6e9c6; +} + +#action-area{ + width: 50%; + float: left; } +#action-area ul{ + display: inline; + list-style: none; + margin: 0; + padding: 0; + line-height: 25px; +} + +#action-area ul li{ + display: inline; +} + +.comment-submit-area-note{ + text-align: right; + font-size: 11px; + margin-bottom: 20px; + color: #333; +} +.comment-submit-area-option{ + width: 100%; + margin-bottom: 10px; + margin-top: 0; + display: inline-block; +} + +.comment-submit-area-option label{ + cursor: pointer; +} + +#file_selector{ + width: 270px; + font-size: 12px; +} + +.webcomment_comment_title span.extra-info { + padding: 2px; + background: #ddd; + margin-left: 10px; + border: 1px solid #ccc; + border-left-width: 3px; + border-left-color: #3c763d; + color: #444; +} +.submit-area input.adminbutton { + margin-left: 0;i +} /* end of invenio.css */ From afb7a47a80488353a74f2ed731f12006d7d612e9 Mon Sep 17 00:00:00 2001 From: Martin Vesper <maves@pb-d-128-141-174-136.cern.ch> Date: Fri, 13 Mar 2015 17:15:54 +0100 Subject: [PATCH 48/83] bibcirculation: Adds ILL and purchase confirmation * Adds e-mail confirmation to the user when an ILL or purchase request is performed. Signed-off-by: Martin Vesper <martin.vesper@cern.ch> --- .../lib/bibcirculation_config.py | 14 +++++++- .../lib/bibcirculation_webinterface.py | 26 +++++++++++++++ .../lib/bibcirculationadminlib.py | 33 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/modules/bibcirculation/lib/bibcirculation_config.py b/modules/bibcirculation/lib/bibcirculation_config.py index 55e7563fe5..40f35e4b5c 100644 --- a/modules/bibcirculation/lib/bibcirculation_config.py +++ b/modules/bibcirculation/lib/bibcirculation_config.py @@ -149,6 +149,7 @@ 'We will process your order of the document immediately and will contact you as soon as it is delivered.\n\n'\ 'Best regards,\nCERN Library team\n', + 'PURCHASE_RECEIVED_TID': 'Dear colleague,\n\n'\ 'The document you requested has been received. '\ 'The price is %s'\ @@ -221,7 +222,6 @@ 'Thank you in advance for your cooperation, CERN Library Staff', 'EMPTY': 'Please choose one template' } - else: CFG_BIBCIRCULATION_TEMPLATES = { 'OVERDUE': 'Overdue letter template (write some text)', @@ -375,6 +375,18 @@ 'EMPTY': 'Please choose one template' } +ill_conf = ('Dear colleague,\n\n' + 'We have received your interlibrary loan request\n' + '\tTitle: {0}\n\n' + 'We will process your order of the document immediately and will ' + 'contact you as soon as it is delivered.\n\n' + 'If you have any questions about your request, please contact ' + '{1}\n\n' + 'Best regards,\n' + 'CERN Library team') + +CFG_BIBCIRCULATION_TEMPLATES['ILL_CONFIRMATION'] = ill_conf + if CFG_CERN_SITE == 1: CFG_BIBCIRCULATION_ILLS_EMAIL = 'CERN External loans<external.loans@cern.ch>' CFG_BIBCIRCULATION_LIBRARIAN_EMAIL = 'CERN Library Desk<library.desk@cern.ch>' diff --git a/modules/bibcirculation/lib/bibcirculation_webinterface.py b/modules/bibcirculation/lib/bibcirculation_webinterface.py index c90f95cc36..bcc4b1ab37 100644 --- a/modules/bibcirculation/lib/bibcirculation_webinterface.py +++ b/modules/bibcirculation/lib/bibcirculation_webinterface.py @@ -481,6 +481,19 @@ def book_request_step3(self, req, form): str(ill_request_notes), only_edition, 'book', budget_code) + borrower_email = db.get_invenio_user_email(uid) + ill_conf_msg = load_template('ill_confirmation') + ill_conf_msg = ill_conf_msg.format(title, + CFG_BIBCIRCULATION_ILLS_EMAIL) + send_email(fromaddr=CFG_BIBCIRCULATION_ILLS_EMAIL, + toaddr=borrower_email, + subject=_("Your inter library loan request"), + header='', + footer='', + content=ill_conf_msg, + attempt_times=1, + attempt_sleeptime=10) + infos = [] infos.append('Interlibrary loan request done.') body = bc_templates.tmpl_infobox(infos, ln) @@ -598,6 +611,19 @@ def article_request_step2(self, req, form): str(ill_request_notes), 'No', 'article', argd['budget_code']) + borrower_email = db.get_invenio_user_email(uid) + ill_conf_msg = load_template('ill_confirmation') + ill_conf_msg = ill_conf_msg.format(argd['article_title'], + CFG_BIBCIRCULATION_ILLS_EMAIL) + send_email(fromaddr=CFG_BIBCIRCULATION_ILLS_EMAIL, + toaddr=borrower_email, + subject=_("Your inter library loan request"), + header='', + footer = '', + content=ill_conf_msg, + attempt_times=1, + attempt_sleeptime=10) + infos = [] infos.append('Interlibrary loan request done.') body = bc_templates.tmpl_infobox(infos, argd['ln']) diff --git a/modules/bibcirculation/lib/bibcirculationadminlib.py b/modules/bibcirculation/lib/bibcirculationadminlib.py index c5647a8b88..0d872c079e 100644 --- a/modules/bibcirculation/lib/bibcirculationadminlib.py +++ b/modules/bibcirculation/lib/bibcirculationadminlib.py @@ -151,6 +151,9 @@ def load_template(template): elif template == "ill_recall3": output = CFG_BIBCIRCULATION_TEMPLATES['ILL_RECALL3'] + elif template == "ill_confirmation": + output = CFG_BIBCIRCULATION_TEMPLATES['ILL_CONFIRMATION'] + elif template == "claim_return": output = CFG_BIBCIRCULATION_TEMPLATES['SEND_RECALL'] @@ -4306,6 +4309,20 @@ def register_ill_request_with_no_recid_step4(req, book_info, borrower_id, str(ill_request_notes), only_edition, 'book', budget_code) + uid = getUid(req) + borrower_email = db.get_invenio_user_email(uid) + ill_conf_msg = load_template('ill_confirmation') + ill_conf_msg = ill_conf_msg.format(title, + CFG_BIBCIRCULATION_ILLS_EMAIL) + send_email(fromaddr=CFG_BIBCIRCULATION_ILLS_EMAIL, + toaddr=borrower_email, + subject=_("Your inter library loan request"), + header='', + footer='', + content=ill_conf_msg, + attempt_times=1, + attempt_sleeptime=10) + return list_ill_request(req, CFG_BIBCIRCULATION_ILL_STATUS_NEW, ln) @@ -4547,6 +4564,8 @@ def register_ill_article_request_step3(req, periodical_title, title, authors, page_number, year, issn, user_info, request_details, ln=CFG_SITE_LANG): + _ = gettext_set_language(ln) + #id_user = getUid(req) (auth_code, auth_message) = is_adminuser(req) if auth_code != 0: @@ -4589,6 +4608,20 @@ def register_ill_article_request_step3(req, periodical_title, title, authors, str(ill_request_notes), only_edition, 'article', budget_code) + uid = getUid(req) + borrower_email = db.get_invenio_user_email(uid) + ill_conf_msg = load_template('ill_confirmation') + ill_conf_msg = ill_conf_msg.format(title, + CFG_BIBCIRCULATION_ILLS_EMAIL) + send_email(fromaddr=CFG_BIBCIRCULATION_ILLS_EMAIL, + toaddr=borrower_email, + subject=_("Your inter library loan request"), + header='', + footer='', + content=ill_conf_msg, + attempt_times=1, + attempt_sleeptime=10) + return list_ill_request(req, CFG_BIBCIRCULATION_ILL_STATUS_NEW, ln) From f871481f936aed5694f1566573b6e6a894197484 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski <sebastian.witowski@cern.ch> Date: Thu, 26 Mar 2015 13:55:25 +0100 Subject: [PATCH 49/83] Websession: Improvements to inveniogc * Adds oairepository_, websubmit_upload_interface_config_ directories and multiedit_*.xml, batchupload_*.xml, webupload_* (the last one only for CERN website) files to the list of removed files. * Fixes the removal of Websubmit icons and Websubmit stamps (those are directories, not single files). Signed-off-by: Sebastian Witowski <sebastian.witowski@cern.ch> --- modules/websession/lib/inveniogc.py | 58 ++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/modules/websession/lib/inveniogc.py b/modules/websession/lib/inveniogc.py index da79895875..358f41eaaf 100644 --- a/modules/websession/lib/inveniogc.py +++ b/modules/websession/lib/inveniogc.py @@ -31,7 +31,7 @@ from invenio.config import CFG_LOGDIR, CFG_TMPDIR, CFG_CACHEDIR, \ CFG_TMPSHAREDDIR, CFG_WEBSEARCH_RSS_TTL, CFG_PREFIX, \ CFG_WEBSESSION_NOT_CONFIRMED_EMAIL_ADDRESS_EXPIRE_IN_DAYS, \ - CFG_INSPIRE_SITE + CFG_INSPIRE_SITE, CFG_CERN_SITE from invenio.bibtask import task_init, task_set_option, task_get_option, \ write_message, write_messages from invenio.bibtask_config import CFG_BIBSCHED_LOGDIR @@ -61,6 +61,8 @@ CFG_MAX_ATIME_RM_OAI = 14 # After how many days to zip obsolete oaiharvest fmt xml files CFG_MAX_ATIME_ZIP_OAI = 3 +# After how many days to remove obsolete oairepository xml files +CFG_MAX_ATIME_RM_OAIREPOSITORY = 14 # After how many days to remove deleted bibdocs CFG_DELETED_BIBDOC_MAXLIFE = 365 * 10 # After how many day to remove old cached webjournal files @@ -81,6 +83,9 @@ # After how many days to remove obsolete WebSubmit-created temporary # stamp files CFG_MAX_ATIME_RM_STAMP = 7 +# After how many days to remove obsolete websubmit_upload_interface_config +# directories created by Document File Manager +CFG_MAX_ATIME_RM_WEBSUBMIT_UPLOAD_INTERFACE = 30 # After how many days to remove obsolete WebJournal-created update XML CFG_MAX_ATIME_RM_WEBJOURNAL_XML = 7 # After how many days to remove obsolete temporary files attached with @@ -91,6 +96,14 @@ CFG_MAX_ATIME_BIBEDIT_TMP = 3 # After how many days to remove submitted XML files related to BibEdit CFG_MAX_ATIME_BIBEDIT_XML = 3 +# After how many days to remove submitted XML files related to MultiEdit +CFG_MAX_ATIME_MULTIEDIT_XML = 3 +# After how many days to remove obsolete batchupload xml files +CFG_MAX_ATIME_BATCHUPLOAD = 7 +# After how many days to remove obsolete webupload-created xml files +CFG_MAX_ATIME_WEBUPLOAD = 7 +# After how many days to remove obsolete mediaarchive xml files +CFG_MAX_ATIME_MEDIAARCHIVE = 7 def gc_exec_command(command): """ Exec the command logging in appropriate way its output.""" @@ -140,6 +153,9 @@ def clean_tempfiles(): ' -mtime +%s -exec rm %s -rf {} \;' \ % (CFG_TMPDIR, CFG_TMPSHAREDDIR, \ CFG_MAX_ATIME_RM_OAI, vstr)) + gc_exec_command('find %s -name "oairepository_*"' + ' -mtime +%s -exec rm %s -rf {} \;' \ + % (CFG_TMPSHAREDDIR, CFG_MAX_ATIME_RM_OAIREPOSITORY, vstr)) if not CFG_INSPIRE_SITE: write_message("- deleting/gzipping temporary old " @@ -172,16 +188,23 @@ def clean_tempfiles(): % (CFG_TMPDIR, CFG_TMPSHAREDDIR, \ CFG_MAX_ATIME_RM_BIBDOC, vstr)) - write_message("- deleting old temporary WebSubmit icons") + write_message("- deleting old temporary WebSubmit icons folders") gc_exec_command('find %s %s -name "websubmit_icon_creator_*"' - ' -atime +%s -exec rm %s -f {} \;' \ + ' -atime +%s -exec rm %s -rf {} \;' \ % (CFG_TMPDIR, CFG_TMPSHAREDDIR, \ CFG_MAX_ATIME_RM_ICON, vstr)) + # Using -r here to remove directories + write_message("- deleting old websubmit_upload_interface_config files") + gc_exec_command('find %s -name "websubmit_upload_interface_config_*"' + ' -atime +%s -exec rm %s -rf {} \;' \ + % (CFG_TMPSHAREDDIR, \ + CFG_MAX_ATIME_RM_WEBSUBMIT_UPLOAD_INTERFACE, vstr)) + if not CFG_INSPIRE_SITE: - write_message("- deleting old temporary WebSubmit stamps") + write_message("- deleting old temporary WebSubmit stamps folders") gc_exec_command('find %s %s -name "websubmit_file_stamper_*"' - ' -atime +%s -exec rm %s -f {} \;' \ + ' -atime +%s -exec rm %s -rf {} \;' \ % (CFG_TMPDIR, CFG_TMPSHAREDDIR, \ CFG_MAX_ATIME_RM_STAMP, vstr)) @@ -203,6 +226,31 @@ def clean_tempfiles(): % (CFG_TMPSHAREDDIR + '/bibedit-cache/', CFG_MAX_ATIME_BIBEDIT_XML, vstr)) + write_message("- deleting old XML files submitted via MultiEdit") + gc_exec_command('find %s -name "multiedit_*.xml"' + ' -atime +%s -exec rm %s -f {} \;' \ + % (CFG_TMPSHAREDDIR, CFG_MAX_ATIME_MULTIEDIT_XML, vstr)) + + write_message("- deleting old XML files submitted via Batchupload") + gc_exec_command('find %s -name "batchupload_*.xml"' + ' -atime +%s -exec rm %s -f {} \;' \ + % (CFG_TMPSHAREDDIR, CFG_MAX_ATIME_BATCHUPLOAD, vstr)) + + if CFG_CERN_SITE: + # Remove some CDS-related temporary files + write_message("- deleting webupload files") + gc_exec_command('find %s -name "webupload_*"' + ' -atime +%s -exec rm %s -f {} \;' \ + % (CFG_TMPSHAREDDIR, CFG_MAX_ATIME_WEBUPLOAD, + vstr)) + + write_message("- deleting mediaarchive files") + gc_exec_command('find %s/mediaarchive/ -name "mediaarchive_*.xml"' + ' -atime +%s -exec rm %s -f {} \;' \ + % (CFG_TMPSHAREDDIR, CFG_MAX_ATIME_MEDIAARCHIVE, + vstr)) + + write_message("""CLEANING OF TMP FILES FINISHED""") def clean_cache(): From 48d5bbb90d80836997545f6cefb5b36f0a799b37 Mon Sep 17 00:00:00 2001 From: Martin Vesper <martin.vesper@cern.ch> Date: Mon, 30 Mar 2015 11:00:06 +0200 Subject: [PATCH 50/83] bibcirculation: Change ILL/purchase e-mail * Changes the 'CFG_BIBCIRCULATION_ILLS_EMAIL' parameter from 'external.loans@cern.ch' to 'lib.acq@cern.ch' Signed-off-by: Martin Vesper <martin.vesper@cern.ch> --- modules/bibcirculation/lib/bibcirculation_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bibcirculation/lib/bibcirculation_config.py b/modules/bibcirculation/lib/bibcirculation_config.py index 55e7563fe5..270486c965 100644 --- a/modules/bibcirculation/lib/bibcirculation_config.py +++ b/modules/bibcirculation/lib/bibcirculation_config.py @@ -376,7 +376,7 @@ } if CFG_CERN_SITE == 1: - CFG_BIBCIRCULATION_ILLS_EMAIL = 'CERN External loans<external.loans@cern.ch>' + CFG_BIBCIRCULATION_ILLS_EMAIL = 'CERN External loans<lib.acq@cern.ch>' CFG_BIBCIRCULATION_LIBRARIAN_EMAIL = 'CERN Library Desk<library.desk@cern.ch>' CFG_BIBCIRCULATION_LOANS_EMAIL = 'CERN Lib loans<lib.loans@cern.ch>' else: From 9fc95eb68ba2544490dd3a5fce92d6c7cafaa05e Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 28 Jun 2011 16:30:20 +0200 Subject: [PATCH 51/83] WebSearch & WebAlert: Your Searches improvements * Moves page "Your Searches" from WebAlert to WebSearch and under the /yoursearches/ URL. Updates all the respective URLs and function calls. Moves function account_list_searches() from WebAlert to WebSearch. * Separates the popular alerts page from the user searches page and creates new separate function for popular alerts the user can choose from to set up new alerts. * Fixes various doc strings, variable and function names. (closes #880) --- .../doc/admin/webalert-admin-guide.webdoc | 2 +- modules/webalert/lib/webalert.py | 138 ++++------- modules/webalert/lib/webalert_templates.py | 227 ++++++++---------- modules/webalert/lib/webalert_webinterface.py | 174 ++++++++------ modules/websearch/lib/Makefile.am | 1 + modules/websearch/lib/websearch_templates.py | 87 +++++++ .../websearch/lib/websearch_webinterface.py | 81 ++++++- .../websearch/lib/websearch_yoursearches.py | 115 +++++++++ .../websession/lib/websession_templates.py | 14 +- .../websession/lib/websession_webinterface.py | 3 +- modules/webstat/etc/webstat.cfg | 4 +- modules/webstat/lib/webstatadmin.py | 4 +- modules/webstyle/lib/webinterface_layout.py | 10 +- 13 files changed, 551 insertions(+), 309 deletions(-) create mode 100644 modules/websearch/lib/websearch_yoursearches.py diff --git a/modules/webalert/doc/admin/webalert-admin-guide.webdoc b/modules/webalert/doc/admin/webalert-admin-guide.webdoc index 570843348b..5ba2a2d305 100644 --- a/modules/webalert/doc/admin/webalert-admin-guide.webdoc +++ b/modules/webalert/doc/admin/webalert-admin-guide.webdoc @@ -53,7 +53,7 @@ module to permit this functionality. <h2>Configuring Alert Queries</h2> <p>Users may set up alert queries for example from their <a -href="<CFG_SITE_URL>/youralerts/display">search history</a> pages. +href="<CFG_SITE_URL>/yoursearches/display">search history</a> pages. <p>Administrators may edit existing users' alerts by modifying the <code>user_query_basket</code> table. (There is no web interface yet diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 8fd46db718..3b51db2f50 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -54,8 +54,10 @@ def check_alert_name(alert_name, uid, ln=CFG_SITE_LANG): raise AlertError( _("You already have an alert named %s.") % ('<b>' + cgi.escape(alert_name) + '</b>',) ) def get_textual_query_info_from_urlargs(urlargs, ln=CFG_SITE_LANG): - """Return nicely formatted search pattern and catalogue from urlargs of the search query. - Suitable for 'your searches' display.""" + """ + Return nicely formatted search pattern and catalogue from urlargs of the search query. + """ + out = "" args = cgi.parse_qs(urlargs) return webalert_templates.tmpl_textual_query_info_from_urlargs( @@ -64,71 +66,6 @@ def get_textual_query_info_from_urlargs(urlargs, ln=CFG_SITE_LANG): ) return out -def perform_display(permanent, uid, ln=CFG_SITE_LANG): - """display the searches performed by the current user - input: default permanent="n"; permanent="y" display permanent queries(most popular) - output: list of searches in formatted html - """ - # load the right language - _ = gettext_set_language(ln) - - # first detect number of queries: - nb_queries_total = 0 - nb_queries_distinct = 0 - query = "SELECT COUNT(*),COUNT(DISTINCT(id_query)) FROM user_query WHERE id_user=%s" - res = run_sql(query, (uid,), 1) - try: - nb_queries_total = res[0][0] - nb_queries_distinct = res[0][1] - except: - pass - - # query for queries: - params = () - if permanent == "n": - SQL_query = "SELECT DISTINCT(q.id),q.urlargs "\ - "FROM query q, user_query uq "\ - "WHERE uq.id_user=%s "\ - "AND uq.id_query=q.id "\ - "ORDER BY q.id DESC" - params = (uid,) - else: - # permanent="y" - SQL_query = "SELECT q.id,q.urlargs "\ - "FROM query q "\ - "WHERE q.type='p'" - query_result = run_sql(SQL_query, params) - - queries = [] - if len(query_result) > 0: - for row in query_result : - if permanent == "n": - res = run_sql("SELECT DATE_FORMAT(MAX(date),'%%Y-%%m-%%d %%H:%%i:%%s') FROM user_query WHERE id_user=%s and id_query=%s", - (uid, row[0])) - try: - lastrun = res[0][0] - except: - lastrun = _("unknown") - else: - lastrun = "" - queries.append({ - 'id' : row[0], - 'args' : row[1], - 'textargs' : get_textual_query_info_from_urlargs(row[1], ln=ln), - 'lastrun' : lastrun, - }) - - - return webalert_templates.tmpl_display_alerts( - ln = ln, - permanent = permanent, - nb_queries_total = nb_queries_total, - nb_queries_distinct = nb_queries_distinct, - queries = queries, - guest = isGuestUser(uid), - guesttxt = warning_guest_user(type="alerts", ln=ln) - ) - def check_user_can_add_alert(id_user, id_query): """Check if ID_USER has really alert adding rights on ID_QUERY (that is, the user made the query herself or the query is one of @@ -236,11 +173,20 @@ def perform_add_alert(alert_name, frequency, notification, run_sql(query, params) out = _("The alert %s has been added to your profile.") out %= '<b>' + cgi.escape(alert_name) + '</b>' - out += perform_list_alerts(uid, ln=ln) + out += perform_request_youralerts_display(uid, ln=ln) return out -def perform_list_alerts(uid, ln=CFG_SITE_LANG): - """perform_list_alerts display the list of alerts for the connected user""" +def perform_request_youralerts_display(uid, + ln=CFG_SITE_LANG): + """ + Display a list of the user defined alerts. + @param uid: The user id + @type uid: int + @param ln: The interface language + @type ln: string + @return: HTML formatted list of the user defined alerts. + """ + # set variables out = "" @@ -284,9 +230,10 @@ def perform_list_alerts(uid, ln=CFG_SITE_LANG): register_exception(alert_admin=True) # link to the "add new alert" form - out = webalert_templates.tmpl_list_alerts(ln=ln, alerts=alerts, - guest=isGuestUser(uid), - guesttxt=warning_guest_user(type="alerts", ln=ln)) + out = webalert_templates.tmpl_youralerts_display(ln=ln, + alerts=alerts, + guest=isGuestUser(uid), + guesttxt=warning_guest_user(type="alerts", ln=ln)) return out def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG): @@ -314,7 +261,7 @@ def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG) out += "The alert <b>%s</b> has been removed from your profile.<br /><br />\n" % cgi.escape(alert_name) else: out += "Unable to remove alert <b>%s</b>.<br /><br />\n" % cgi.escape(alert_name) - out += perform_list_alerts(uid, ln=ln) + out += perform_request_youralerts_display(uid, ln=ln) return out @@ -374,7 +321,7 @@ def perform_update_alert(alert_name, frequency, notification, id_basket, id_quer run_sql(query, params) out += _("The alert %s has been successfully updated.") % ("<b>" + cgi.escape(alert_name) + "</b>",) - out += "<br /><br />\n" + perform_list_alerts(uid, ln=ln) + out += "<br /><br />\n" + perform_request_youralerts_display(uid, ln=ln) return out def is_selected(var, fld): @@ -409,21 +356,32 @@ def account_list_alerts(uid, ln=CFG_SITE_LANG): return webalert_templates.tmpl_account_list_alerts(ln=ln, alerts=alerts) -def account_list_searches(uid, ln=CFG_SITE_LANG): - """ account_list_searches: list the searches of the user - input: the user id - output: resume of the searches""" - out = "" - # first detect number of queries: - nb_queries_total = 0 - res = run_sql("SELECT COUNT(*) FROM user_query WHERE id_user=%s", (uid,), 1) - try: - nb_queries_total = res[0][0] - except: - pass - +def perform_request_youralerts_popular(ln=CFG_SITE_LANG): + """ + Display popular alerts. + @param uid: the user id + @type uid: integer + @return: A list of searches queries in formatted html. + """ + # load the right language _ = gettext_set_language(ln) - out += _("You have made %(x_nb)s queries. A %(x_url_open)sdetailed list%(x_url_close)s is available with a possibility to (a) view search results and (b) subscribe to an automatic email alerting service for these queries.") % {'x_nb': nb_queries_total, 'x_url_open': '<a href="../youralerts/display?ln=%s">' % ln, 'x_url_close': '</a>'} - return out + # fetch the popular queries + query = """ SELECT q.id, + q.urlargs + FROM query q + WHERE q.type='p'""" + result = run_sql(query) + + search_queries = [] + if result: + for search_query in result: + search_query_id = search_query[0] + search_query_args = search_query[1] + search_queries.append({'id' : search_query_id, + 'args' : search_query_args, + 'textargs' : get_textual_query_info_from_urlargs(search_query_args, ln=ln)}) + + return webalert_templates.tmpl_youralerts_popular(ln = ln, + search_queries = search_queries) diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 80e75bf05b..934e7113f6 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -107,7 +107,7 @@ def tmpl_account_list_alerts(self, ln, alerts): # load the right message language _ = gettext_set_language(ln) - out = """<form name="displayalert" action="../youralerts/list" method="post"> + out = """<form name="displayalert" action="../youralerts/display" method="post"> %(you_own)s: <select name="id_alert"> <option value="0">- %(alert_name)s -</option>""" % { @@ -257,50 +257,46 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio return out - def tmpl_list_alerts(self, ln, alerts, guest, guesttxt): + def tmpl_youralerts_display(self, + ln, + alerts, + guest, + guesttxt): """ Displays the list of alerts - Parameters: - - - 'ln' *string* - The language to display the interface in - - - 'alerts' *array* - The existing alerts: - - - 'queryid' *string* - The id of the associated query - - - 'queryargs' *string* - The query string - - - 'textargs' *string* - The textual description of the query string - - - 'userid' *string* - The user id - - - 'basketid' *string* - The basket id - - - 'basketname' *string* - The basket name - - - 'alertname' *string* - The alert name - - - 'frequency' *string* - The frequency of alert running ('day', 'week', 'month') - - - 'notification' *string* - If notification should be sent by email ('y', 'n') - - - 'created' *string* - The date of alert creation - - - 'lastrun' *string* - The last running date - - - 'guest' *bool* - If the user is a guest user - - - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) + @param ln: The language to display the interface in + @type ln: string + + @param alerts: The user's alerts. A list of dictionaries each one consisting of: + 'queryid' *string* - The id of the associated query + 'queryargs' *string* - The query string + 'textargs' *string* - The textual description of the query string + 'userid' *string* - The user id + 'basketid' *string* - The basket id + 'basketname' *string* - The basket name + 'alertname' *string* - The alert name + 'frequency' *string* - The frequency of alert running ('day', 'week', 'month') + 'notification' *string* - If notification should be sent by email ('y', 'n') + 'created' *string* - The date of alert creation + 'lastrun' *string* - The last running date + @type alerts: list of dictionaries + + @param guest: Whether the user is a guest or not + @type guest: boolean + + @param guesttxt: The HTML content of the warning box for guest users + (produced by webaccount.tmpl_warning_guest_user) + @type guesttxt: string """ # load the right message language _ = gettext_set_language(ln) - out = '<p>' + _("Set a new alert from %(x_url1_open)syour searches%(x_url1_close)s, the %(x_url2_open)spopular searches%(x_url2_close)s, or the input form.") + '</p>' - out %= {'x_url1_open': '<a href="display?ln=' + ln + '">', + out = '<p>' + _("Set a new alert from %(x_url1_open)syour searches%(x_url1_close)s, the %(x_url2_open)spopular alerts%(x_url2_close)s, or the input form.") + '</p>' + out %= {'x_url1_open': '<a href="' + CFG_SITE_URL + '/yoursearches/display?ln=' + ln + '">', 'x_url1_close': '</a>', - 'x_url2_open': '<a href="display?ln=' + ln + '&amp;p=y">', + 'x_url2_open': '<a href="' + CFG_SITE_URL + '/youralerts/popular?ln=' + ln + '">', 'x_url2_close': '</a>', } if len(alerts): @@ -401,97 +397,6 @@ def tmpl_list_alerts(self, ln, alerts, guest, guesttxt): out += guesttxt return out - def tmpl_display_alerts(self, ln, permanent, nb_queries_total, nb_queries_distinct, queries, guest, guesttxt): - """ - Displays the list of alerts - - Parameters: - - - 'ln' *string* - The language to display the interface in - - - 'permanent' *string* - If displaying most popular searches ('y') or only personal searches ('n') - - - 'nb_queries_total' *string* - The number of personal queries in the last period - - - 'nb_queries_distinct' *string* - The number of distinct queries in the last period - - - 'queries' *array* - The existing queries: - - - 'id' *string* - The id of the associated query - - - 'args' *string* - The query string - - - 'textargs' *string* - The textual description of the query string - - - 'lastrun' *string* - The last running date (only for personal queries) - - - 'guest' *bool* - If the user is a guest user - - - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) - """ - - # load the right message language - _ = gettext_set_language(ln) - - if len(queries) == 0: - out = _("You have not executed any search yet. Please go to the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ - {'x_url_open': '<a href="' + CFG_SITE_URL + '/?ln=' + ln +'">', - 'x_url_close': '</a>'} - return out - - out = '' - - # display message: number of items in the list - if permanent == "n": - msg = _("You have performed %(x_nb1)s searches (%(x_nb2)s different questions) during the last 30 days or so.") % {'x_nb1': nb_queries_total, - 'x_nb2': nb_queries_distinct} - out += '<p>' + msg + '</p>' - else: - # permanent="y" - msg = _("Here are the %s most popular searches.") - msg %= ('<b>' + str(len(queries)) + '</b>') - out += '<p>' + msg + '</p>' - - # display the list of searches - out += """<table class="alrtTable"> - <tr class="pageboxlefttop"> - <td style="font-weight: bold">%(no)s</td> - <td style="font-weight: bold">%(question)s</td> - <td style="font-weight: bold">%(action)s</td>""" % { - 'no' : "#", - 'question' : _("Question"), - 'action' : _("Action") - } - if permanent == "n": - out += '<td style="font-weight: bold">%s</td>' % _("Last Run") - out += "</tr>\n" - i = 0 - for query in queries : - i += 1 - # id, pattern, base, search url and search set alert, date - out += """<tr> - <td style="font-style: italic;">#%(index)d</td> - <td>%(textargs)s</td> - <td><a href="%(siteurl)s/search?%(args)s">%(execute_query)s</a><br /> - <a href="%(siteurl)s/youralerts/input?ln=%(ln)s&amp;idq=%(id)d">%(set_alert)s</a></td>""" % { - 'index' : i, - 'textargs' : query['textargs'], - 'siteurl' : CFG_SITE_URL, - 'args' : cgi.escape(query['args']), - 'id' : query['id'], - 'ln': ln, - 'execute_query' : _("Execute search"), - 'set_alert' : _("Set new alert") - } - if permanent == "n": - out += '<td>%s</td>' % query['lastrun'] - out += """</tr>\n""" - out += "</table><br />\n" - if guest : - out += guesttxt - - return out - def tmpl_alert_email_title(self, name): return 'Alert %s run on %s' % ( name, time.strftime("%Y-%m-%d")) @@ -641,7 +546,7 @@ def tmpl_alert_email_body(self, name, description, url, records, pattern, %s Alert Service <%s> Unsubscribe? See <%s> Need human intervention? Contact <%s> -''' % (CFG_SITE_NAME, CFG_SITE_URL, CFG_SITE_URL + '/youralerts/list', CFG_SITE_SUPPORT_EMAIL) +''' % (CFG_SITE_NAME, CFG_SITE_URL, CFG_SITE_URL + '/youralerts/display', CFG_SITE_SUPPORT_EMAIL) return body @@ -656,3 +561,69 @@ def tmpl_alert_email_record(self, recid=0, xml_record=None): out = wrap_records(get_as_text(xml_record=xml_record)) # TODO: add Detailed record url for external records? return out + + def tmpl_youralerts_popular(self, + ln, + search_queries): + """ + Display the popular alerts. + + Parameters: + + - 'ln' *string* - The language to display the interface in + + - 'search_queries' *array* - The existing queries: + + - 'id' *string* - The id of the associated query + + - 'args' *string* - The query string + + - 'textargs' *string* - The textual description of the query string + + - 'guest' *bool* - If the user is a guest user + + - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) + """ + + # load the right message language + _ = gettext_set_language(ln) + + if not search_queries: + out = _("There are no popular alerts defined yet.") + return out + + out = '' + + # display the list of searches + out += """<table class="alrtTable"> + <tr class="pageboxlefttop"> + <td style="font-weight: bold">%(no)s</td> + <td style="font-weight: bold">%(question)s</td> + <td style="font-weight: bold">%(action)s</td>""" % { + 'no' : "#", + 'question' : _("Question"), + 'action' : _("Action") + } + out += "</tr>\n" + i = 0 + for search_query in search_queries : + i += 1 + # id, pattern, base, search url and search set alert + out += """<tr> + <td style="font-style: italic;">#%(index)d</td> + <td>%(textargs)s</td> + <td><a href="%(siteurl)s/search?%(args)s">%(execute_query)s</a><br /> + <a href="%(siteurl)s/youralerts/input?ln=%(ln)s&amp;idq=%(id)d">%(set_alert)s</a></td>""" % { + 'index' : i, + 'textargs' : search_query['textargs'], + 'siteurl' : CFG_SITE_URL, + 'args' : cgi.escape(search_query['args']), + 'id' : search_query['id'], + 'ln': ln, + 'execute_query' : _("Execute search"), + 'set_alert' : _("Set new alert") + } + out += """</tr>\n""" + out += "</table><br />\n" + + return out diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index 5c8a3a5987..a7afc94dbf 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -24,7 +24,13 @@ from invenio.config import CFG_SITE_SECURE_URL, CFG_SITE_NAME, \ CFG_ACCESS_CONTROL_LEVEL_SITE, CFG_SITE_NAME_INTL from invenio.webpage import page -from invenio import webalert +from invenio.webalert import perform_input_alert, \ + perform_request_youralerts_display, \ + perform_add_alert, \ + perform_update_alert, \ + perform_remove_alert, \ + perform_request_youralerts_popular, \ + AlertError from invenio.webuser import getUid, page_not_authorized, isGuestUser from invenio.webinterface_handler import wash_urlargd, WebInterfaceDirectory from invenio.urlutils import redirect_to_url, make_canonical_urlargd @@ -39,72 +45,28 @@ class WebInterfaceYourAlertsPages(WebInterfaceDirectory): """Defines the set of /youralerts pages.""" - _exports = ['', 'display', 'input', 'modify', 'list', 'add', - 'update', 'remove'] + _exports = ['', + 'display', + 'input', + 'modify', + 'list', + 'add', + 'update', + 'remove', + 'popular'] def index(self, req, dummy): """Index page.""" - redirect_to_url(req, '%s/youralerts/list' % CFG_SITE_SECURE_URL) - def display(self, req, form): - """Display search history page. A misnomer.""" - - argd = wash_urlargd(form, {'p': (str, "n") - }) - - uid = getUid(req) - - # load the right language - _ = gettext_set_language(argd['ln']) - - if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: - return page_not_authorized(req, "%s/youralerts/display" % \ - (CFG_SITE_SECURE_URL,), - navmenuid="youralerts") - elif uid == -1 or isGuestUser(uid): - return redirect_to_url(req, "%s/youraccount/login%s" % ( - CFG_SITE_SECURE_URL, - make_canonical_urlargd({ - 'referer' : "%s/youralerts/display%s" % ( - CFG_SITE_SECURE_URL, - make_canonical_urlargd(argd, {})), - "ln" : argd['ln']}, {}))) - - user_info = collect_user_info(req) - if not user_info['precached_usealerts']: - return page_not_authorized(req, "../", \ - text = _("You are not authorized to use alerts.")) + redirect_to_url(req, '%s/youralerts/display' % CFG_SITE_SECURE_URL) - if argd['p'] == 'y': - _title = _("Popular Searches") - else: - _title = _("Your Searches") + def list(self, req, form): + """ + Legacy youralerts list page. + Now redirects to the youralerts display page. + """ - # register event in webstat - if user_info['email']: - user_str = "%s (%d)" % (user_info['email'], user_info['uid']) - else: - user_str = "" - try: - register_customevent("alerts", ["display", "", user_str]) - except: - register_exception(suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") - - return page(title=_title, - body=webalert.perform_display(argd['p'], uid, ln=argd['ln']), - navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { - 'sitesecureurl' : CFG_SITE_SECURE_URL, - 'ln': argd['ln'], - 'account' : _("Your Account"), - }, - description=_("%s Personalize, Display searches") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), - keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), - uid=uid, - language=argd['ln'], - req=req, - lastupdated=__lastupdated__, - navmenuid='youralerts', - secure_page_p=1) + redirect_to_url(req, '%s/youralerts/display' % (CFG_SITE_SECURE_URL,)) def input(self, req, form): @@ -139,9 +101,9 @@ def input(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = webalert.perform_input_alert("add", argd['idq'], argd['name'], argd['freq'], + html = perform_input_alert("add", argd['idq'], argd['name'], argd['freq'], argd['notif'], argd['idb'], uid, ln=argd['ln']) - except webalert.AlertError, msg: + except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { @@ -224,9 +186,9 @@ def modify(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = webalert.perform_input_alert("update", argd['idq'], argd['name'], argd['freq'], + html = perform_input_alert("update", argd['idq'], argd['name'], argd['freq'], argd['notif'], argd['idb'], uid, argd['old_idb'], ln=argd['ln']) - except webalert.AlertError, msg: + except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { @@ -275,21 +237,21 @@ def modify(self, req, form): lastupdated=__lastupdated__, navmenuid='youralerts') - def list(self, req, form): + def display(self, req, form): argd = wash_urlargd(form, {}) uid = getUid(req) if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: - return page_not_authorized(req, "%s/youralerts/list" % \ + return page_not_authorized(req, "%s/youralerts/display" % \ (CFG_SITE_SECURE_URL,), navmenuid="youralerts") elif uid == -1 or isGuestUser(uid): return redirect_to_url(req, "%s/youraccount/login%s" % ( CFG_SITE_SECURE_URL, make_canonical_urlargd({ - 'referer' : "%s/youralerts/list%s" % ( + 'referer' : "%s/youralerts/display%s" % ( CFG_SITE_SECURE_URL, make_canonical_urlargd(argd, {})), "ln" : argd['ln']}, {}))) @@ -307,12 +269,12 @@ def list(self, req, form): else: user_str = "" try: - register_customevent("alerts", ["list", "", user_str]) + register_customevent("alerts", ["display", "", user_str]) except: register_exception(suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") return page(title=_("Your Alerts"), - body=webalert.perform_list_alerts(uid, ln = argd['ln']), + body=perform_request_youralerts_display(uid, ln = argd['ln']), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { 'sitesecureurl' : CFG_SITE_SECURE_URL, 'ln': argd['ln'], @@ -358,9 +320,9 @@ def add(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = webalert.perform_add_alert(argd['name'], argd['freq'], argd['notif'], + html = perform_add_alert(argd['name'], argd['freq'], argd['notif'], argd['idb'], argd['idq'], uid, ln=argd['ln']) - except webalert.AlertError, msg: + except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { @@ -435,9 +397,9 @@ def update(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = webalert.perform_update_alert(argd['name'], argd['freq'], argd['notif'], + html = perform_update_alert(argd['name'], argd['freq'], argd['notif'], argd['idb'], argd['idq'], argd['old_idb'], uid, ln=argd['ln']) - except webalert.AlertError, msg: + except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { @@ -509,9 +471,9 @@ def remove(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = webalert.perform_remove_alert(argd['name'], argd['idq'], + html = perform_remove_alert(argd['name'], argd['idq'], argd['idb'], uid, ln=argd['ln']) - except webalert.AlertError, msg: + except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { @@ -554,3 +516,63 @@ def remove(self, req, form): req=req, lastupdated=__lastupdated__, navmenuid='youralerts') + + def popular(self, req, form): + """ + Display a list of popular alerts. + """ + + argd = wash_urlargd(form, {'ln': (str, "en")}) + + uid = getUid(req) + + # load the right language + _ = gettext_set_language(argd['ln']) + + if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "%s/youralerts/popular" % \ + (CFG_SITE_SECURE_URL,), + navmenuid="youralerts") + elif uid == -1 or isGuestUser(uid): + return redirect_to_url(req, "%s/youraccount/login%s" % \ + (CFG_SITE_SECURE_URL, + make_canonical_urlargd( + {'referer' : "%s/youralerts/popular%s" % ( + CFG_SITE_SECURE_URL, + make_canonical_urlargd(argd, {})), + 'ln' : argd['ln']}, + {}) + ) + ) + + user_info = collect_user_info(req) + if not user_info['precached_usealerts']: + return page_not_authorized(req, "../", \ + text = _("You are not authorized to use alerts.")) + + # register event in webstat + if user_info['email']: + user_str = "%s (%d)" % (user_info['email'], user_info['uid']) + else: + user_str = "" + try: + register_customevent("alerts", ["popular", "", user_str]) + except: + register_exception( + suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") + + return page(title=_("Popular Alerts"), + body = perform_request_youralerts_popular(ln=argd['ln']), + navtrail = """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Popular Alerts") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='youralerts', + secure_page_p=1) diff --git a/modules/websearch/lib/Makefile.am b/modules/websearch/lib/Makefile.am index 412b6654d7..e52356d0e8 100644 --- a/modules/websearch/lib/Makefile.am +++ b/modules/websearch/lib/Makefile.am @@ -22,6 +22,7 @@ jsdir=$(localstatedir)/www/js webdir=$(localstatedir)/www/img pylib_DATA = \ + websearch_yoursearches.py \ websearchadminlib.py \ websearch_templates.py \ websearch_webinterface.py \ diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 2c54ea5596..635c576fa3 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5080,3 +5080,90 @@ def restore_search_args_to_default(self, arg_list): if default_values: default_args[item] = default_values[1] return default_args + + def tmpl_yoursearches_display(self, + ln, + nb_queries_total, + nb_queries_distinct, + search_queries, + guest, + guesttxt): + """ + Display the user's search history. + + Parameters: + + - 'ln' *string* - The language to display the interface in + + - 'nb_queries_total' *string* - The number of personal queries in the last period + + - 'nb_queries_distinct' *string* - The number of distinct queries in the last period + + - 'search_queries' *array* - The existing queries: + + - 'id' *string* - The id of the associated query + + - 'args' *string* - The query string + + - 'textargs' *string* - The textual description of the query string + + - 'lastrun' *string* - The last running date (only for personal queries) + + - 'guest' *bool* - If the user is a guest user + + - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) + """ + + # load the right message language + _ = gettext_set_language(ln) + + if not search_queries: + out = _("You have not executed any search yet. Please go to the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ + {'x_url_open': '<a href="' + CFG_SITE_URL + '/?ln=' + ln +'">', + 'x_url_close': '</a>'} + return out + + out = '' + + # display message: number of items in the list + msg = _("You have performed %(x_nb1)s searches (%(x_nb2)s different questions) during the last 30 days or so.") % {'x_nb1': nb_queries_total, + 'x_nb2': nb_queries_distinct} + out += '<p>' + msg + '</p>' + + # display the list of searches + out += """<table class="alrtTable"> + <tr class="pageboxlefttop"> + <td style="font-weight: bold">%(no)s</td> + <td style="font-weight: bold">%(question)s</td> + <td style="font-weight: bold">%(action)s</td>""" % { + 'no' : "#", + 'question' : _("Question"), + 'action' : _("Action") + } + out += '<td style="font-weight: bold">%s</td>' % _("Last Run") + out += "</tr>\n" + i = 0 + for search_query in search_queries : + i += 1 + # id, pattern, base, search url and search set alert, date + out += """<tr> + <td style="font-style: italic;">#%(index)d</td> + <td>%(textargs)s</td> + <td><a href="%(siteurl)s/search?%(args)s">%(execute_query)s</a><br /> + <a href="%(siteurl)s/youralerts/input?ln=%(ln)s&amp;idq=%(id)d">%(set_alert)s</a></td>""" % { + 'index' : i, + 'textargs' : search_query['textargs'], + 'siteurl' : CFG_SITE_URL, + 'args' : cgi.escape(search_query['args']), + 'id' : search_query['id'], + 'ln': ln, + 'execute_query' : _("Execute search"), + 'set_alert' : _("Set new alert") + } + out += '<td>%s</td>' % search_query['lastrun'] + out += """</tr>\n""" + out += "</table><br />\n" + if guest : + out += guesttxt + + return out diff --git a/modules/websearch/lib/websearch_webinterface.py b/modules/websearch/lib/websearch_webinterface.py index d016f66ede..9c2bfdee12 100644 --- a/modules/websearch/lib/websearch_webinterface.py +++ b/modules/websearch/lib/websearch_webinterface.py @@ -18,6 +18,7 @@ """WebSearch URL handler.""" __revision__ = "$Id$" +__lastupdated__ = """$Date$""" import cgi import os @@ -56,14 +57,17 @@ CFG_WEBSEARCH_RSS_I18N_COLLECTIONS, \ CFG_INSPIRE_SITE, \ CFG_WEBSEARCH_WILDCARD_LIMIT, \ - CFG_SITE_RECORD + CFG_SITE_RECORD, \ + CFG_ACCESS_CONTROL_LEVEL_SITE, \ + CFG_SITE_NAME_INTL + from invenio.dbquery import Error from invenio.webinterface_handler import wash_urlargd, WebInterfaceDirectory from invenio.urlutils import redirect_to_url, make_canonical_urlargd, drop_default_urlargd from invenio.htmlutils import get_mathjax_header from invenio.htmlutils import nmtoken_from_string from invenio.webuser import getUid, page_not_authorized, get_user_preferences, \ - collect_user_info, logoutUser, isUserSuperAdmin + collect_user_info, logoutUser, isUserSuperAdmin, isGuestUser from invenio.webcomment_webinterface import WebInterfaceCommentsPages from invenio.weblinkback_webinterface import WebInterfaceRecordLinkbacksPages from invenio.bibcirculation_webinterface import WebInterfaceHoldingsPages @@ -108,6 +112,7 @@ from invenio.bibdocfile_webinterface import WebInterfaceManageDocFilesPages, WebInterfaceFilesPages from invenio.bibfield import get_record from invenio.shellutils import mymkdir +from invenio.websearch_yoursearches import perform_request_yoursearches_display import invenio.template websearch_templates = invenio.template.load('websearch') @@ -1196,3 +1201,75 @@ def __call__(self, req, form): # Return the same page wether we ask for /CFG_SITE_RECORD/123/export/xm or /CFG_SITE_RECORD/123/export/xm/ index = __call__ + +class WebInterfaceYourSearchesPages(WebInterfaceDirectory): + """ + Handles the /yoursearches pages + """ + + _exports = ['', 'display'] + + def index(self, req, form): + """ + """ + redirect_to_url(req, '%s/yoursearches/display' % CFG_SITE_URL) + + def display(self, req, form): + """ + Display the user's search latest history. + """ + + argd = wash_urlargd(form, {'ln': (str, "en")}) + + uid = getUid(req) + + # load the right language + _ = gettext_set_language(argd['ln']) + + if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "%s/yoursearches/display" % \ + (CFG_SITE_URL,), + navmenuid="yoursearches") + elif uid == -1 or isGuestUser(uid): + return redirect_to_url(req, "%s/youraccount/login%s" % \ + (CFG_SITE_SECURE_URL, + make_canonical_urlargd( + {'referer' : "%s/yoursearches/display%s" % ( + CFG_SITE_URL, + make_canonical_urlargd(argd, {})), + 'ln' : argd['ln']}, + {}) + ) + ) + + user_info = collect_user_info(req) + #if not user_info['precached_usealerts']: + # return page_not_authorized(req, "../", \ + # text = _("You are not authorized to use alerts.")) + + # register event in webstat + if user_info['email']: + user_str = "%s (%d)" % (user_info['email'], user_info['uid']) + else: + user_str = "" + try: + register_customevent("searches", ["display", "", user_str]) + except: + register_exception( + suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") + + return page(title=_("Your Searches"), + body=perform_request_yoursearches_display(uid, ln=argd['ln']), + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Display searches") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='yoursearches', + secure_page_p=1) diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py new file mode 100644 index 0000000000..e655c3dff5 --- /dev/null +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -0,0 +1,115 @@ +## 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. + +"""User searches personal features.""" + +__revision__ = "$Id$" + +from invenio.config import CFG_SITE_LANG, CFG_SITE_URL +from invenio.dbquery import run_sql +from invenio.webaccount import warning_guest_user +from invenio.webalert import get_textual_query_info_from_urlargs +from invenio.messages import gettext_set_language +from invenio.webuser import isGuestUser + +import invenio.template +websearch_templates = invenio.template.load('websearch') + +def perform_request_yoursearches_display(uid, + ln=CFG_SITE_LANG): + """ + Display the user's search history. + @param uid: the user id + @type uid: integer + @return: A list of searches queries in formatted html. + """ + + # load the right language + _ = gettext_set_language(ln) + + # firstly, calculate the number of total and distinct queries + nb_queries_total = 0 + nb_queries_distinct = 0 + query_nb_queries = """ SELECT COUNT(*), + COUNT(DISTINCT(id_query)) + FROM user_query + WHERE id_user=%s""" + params_nb_queries = (uid,) + res_nb_queries = run_sql(query_nb_queries, params_nb_queries) + nb_queries_total = res_nb_queries[0][0] + nb_queries_distinct = res_nb_queries[0][1] + + # secondly, calculate the search queries + query = """ SELECT DISTINCT(q.id), + q.urlargs, + DATE_FORMAT(MAX(uq.date),'%%Y-%%m-%%d %%H:%%i:%%s') + FROM query q, + user_query uq + WHERE uq.id_user=%s + AND uq.id_query=q.id + GROUP BY uq.id_query + ORDER BY q.id DESC""" + params = (uid,) + result = run_sql(query, params) + + search_queries = [] + if result: + for search_query in result: + search_query_id = search_query[0] + search_query_args = search_query[1] + search_query_lastrun = search_query[2] or _("unknown") + search_queries.append({'id' : search_query_id, + 'args' : search_query_args, + 'textargs' : get_textual_query_info_from_urlargs(search_query_args, ln=ln), + 'lastrun' : search_query_lastrun}) + + return websearch_templates.tmpl_yoursearches_display( + ln = ln, + nb_queries_total = nb_queries_total, + nb_queries_distinct = nb_queries_distinct, + search_queries = search_queries, + guest = isGuestUser(uid), + guesttxt = warning_guest_user(type="searches", ln=ln)) + +def account_list_searches(uid, + ln=CFG_SITE_LANG): + """ + Display a short summary of the searches the user has performed. + @param uid: The user id + @type uid: int + @return: A short summary of the user searches. + """ + + # load the right language + _ = gettext_set_language(ln) + + query = """ SELECT COUNT(uq.id_query) + FROM user_query uq + WHERE uq.id_user=%s""" + params = (uid,) + result = run_sql(query, params, 1) + if result: + nb_queries_total = result[0][0] + else: + nb_queries_total = 0 + + out = _("You have made %(x_nb)s queries. A %(x_url_open)sdetailed list%(x_url_close)s is available with a possibility to (a) view search results and (b) subscribe to an automatic email alerting service for these queries.") % \ + {'x_nb': nb_queries_total, + 'x_url_open': '<a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_URL, ln), + 'x_url_close': '</a>'} + + return out \ No newline at end of file diff --git a/modules/websession/lib/websession_templates.py b/modules/websession/lib/websession_templates.py index 11e4a32eb9..e0179d6ff9 100644 --- a/modules/websession/lib/websession_templates.py +++ b/modules/websession/lib/websession_templates.py @@ -599,7 +599,7 @@ def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): } out += """ - <dt><a href="../youralerts/display?ln=%(ln)s">%(your_searches)s</a></dt> + <dt><a href="../yoursearches/display?ln=%(ln)s">%(your_searches)s</a></dt> <dd>%(search_explain)s</dd>""" % { 'ln' : ln, 'your_searches' : _("Your Searches"), @@ -622,7 +622,7 @@ def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): 'comments_explain' : _("Display all the comments you have submitted so far."), } out += """</dd> - <dt><a href="../youralerts/list?ln=%(ln)s">%(your_alerts)s</a></dt> + <dt><a href="../youralerts/display?ln=%(ln)s">%(your_alerts)s</a></dt> <dd>%(explain_alerts)s""" % { 'ln' : ln, 'your_alerts' : _("Your Alerts"), @@ -662,6 +662,8 @@ def tmpl_warning_guest_user(self, ln, type): msg = _("You are logged in as a guest user, so your baskets will disappear at the end of the current session.") + ' ' elif (type=='alerts'): msg = _("You are logged in as a guest user, so your alerts will disappear at the end of the current session.") + ' ' + elif (type=='searches'): + msg = _("You are logged in as a guest user, so your searches will disappear at the end of the current session.") + ' ' msg += _("If you wish you can %(x_url_open)slogin or register here%(x_url_close)s.") % {'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/login?ln=' + ln + '">', 'x_url_close': '</a>'} return """<table class="errorbox" summary=""> @@ -773,10 +775,10 @@ def tmpl_account_page(self, ln, warnings, warning_list, accBody, baskets, alerts 'x_url_close': '</a>'} out += self.tmpl_account_template(_("Your Comments"), comments_description, ln, '/yourcomments/?ln=%s' % ln) if alerts: - out += self.tmpl_account_template(_("Your Alert Searches"), alerts, ln, '/youralerts/list?ln=%s' % ln) + out += self.tmpl_account_template(_("Your Alert Searches"), alerts, ln, '/youralerts/display?ln=%s' % ln) if searches: - out += self.tmpl_account_template(_("Your Searches"), searches, ln, '/youralerts/display?ln=%s' % ln) + out += self.tmpl_account_template(_("Your Searches"), searches, ln, '/yoursearches/display?ln=%s' % ln) if groups: groups_description = _("You can consult the list of %(x_url_open)syour groups%(x_url_close)s you are administering or are a member of.") @@ -1433,7 +1435,7 @@ def tmpl_create_useractivities_menu(self, ln, selected, url_referer, guest, user 'account' : _('Your account') } if usealerts or guest: - out += '<li><a href="%(CFG_SITE_SECURE_URL)s/youralerts/list?ln=%(ln)s">%(alerts)s</a></li>' % { + out += '<li><a href="%(CFG_SITE_SECURE_URL)s/youralerts/display?ln=%(ln)s">%(alerts)s</a></li>' % { 'CFG_SITE_SECURE_URL' : CFG_SITE_SECURE_URL, 'ln' : ln, 'alerts' : _('Your alerts') @@ -1481,7 +1483,7 @@ def tmpl_create_useractivities_menu(self, ln, selected, url_referer, guest, user 'submissions' : _('Your submissions') } if usealerts or guest: - out += '<li><a href="%(CFG_SITE_SECURE_URL)s/youralerts/display?ln=%(ln)s">%(searches)s</a></li>' % { + out += '<li><a href="%(CFG_SITE_SECURE_URL)s/yoursearches/display?ln=%(ln)s">%(searches)s</a></li>' % { 'CFG_SITE_SECURE_URL' : CFG_SITE_SECURE_URL, 'ln' : ln, 'searches' : _('Your searches') diff --git a/modules/websession/lib/websession_webinterface.py b/modules/websession/lib/websession_webinterface.py index a80be1259b..be00e2aa34 100644 --- a/modules/websession/lib/websession_webinterface.py +++ b/modules/websession/lib/websession_webinterface.py @@ -46,6 +46,7 @@ from invenio import webaccount from invenio import webbasket from invenio import webalert +from invenio import websearch_yoursearches from invenio.dbquery import run_sql from invenio.webmessage import account_new_mail from invenio.access_control_engine import acc_authorize_action @@ -241,7 +242,7 @@ def display(self, req, form): user_info = webuser.collect_user_info(req) bask = user_info['precached_usebaskets'] and webbasket.account_list_baskets(uid, ln=args['ln']) or '' aler = user_info['precached_usealerts'] and webalert.account_list_alerts(uid, ln=args['ln']) or '' - sear = webalert.account_list_searches(uid, ln=args['ln']) + sear = websearch_yoursearches.account_list_searches(uid, ln=args['ln']) msgs = user_info['precached_usemessages'] and account_new_mail(uid, ln=args['ln']) or '' grps = user_info['precached_usegroups'] and webgroup.account_group(uid, ln=args['ln']) or '' appr = user_info['precached_useapprove'] diff --git a/modules/webstat/etc/webstat.cfg b/modules/webstat/etc/webstat.cfg index ed28f4bc9c..92c3170d1c 100644 --- a/modules/webstat/etc/webstat.cfg +++ b/modules/webstat/etc/webstat.cfg @@ -72,5 +72,5 @@ add-to-basket-url = "/yourbaskets/add" display-basket-url = "/yourbaskets/display" display-public-basket-url = "/yourbaskets/display_public" alert-url = "/youralerts/" -display-your-alerts-url = "/youralerts/list" -display-your-searches-url = "/youralerts/display" +display-your-alerts-url = "/youralerts/display" +display-your-searches-url = "/yoursearches/display" diff --git a/modules/webstat/lib/webstatadmin.py b/modules/webstat/lib/webstatadmin.py index 0df038b9f8..706a1b8808 100644 --- a/modules/webstat/lib/webstatadmin.py +++ b/modules/webstat/lib/webstatadmin.py @@ -181,8 +181,8 @@ def task_submit_check_options(): display-basket-url = "/yourbaskets/display" display-public-basket-url = "/yourbaskets/display_public" alert-url = "/youralerts/" -display-your-alerts-url = "/youralerts/list" -display-your-searches-url = "/youralerts/display" +display-your-alerts-url = "/youralerts/display" +display-your-searches-url = "/yoursearches/display" """ % CFG_SITE_RECORD sys.exit(0) diff --git a/modules/webstyle/lib/webinterface_layout.py b/modules/webstyle/lib/webinterface_layout.py index 494dbed8d6..409ff35c87 100644 --- a/modules/webstyle/lib/webinterface_layout.py +++ b/modules/webstyle/lib/webinterface_layout.py @@ -109,6 +109,12 @@ def _lookup(self, component, path): register_exception(alert_admin=True, subject='EMERGENCY') WebInterfaceUnAPIPages = WebInterfaceDumbPages +try: + from invenio.websearch_webinterface import WebInterfaceYourSearchesPages +except: + register_exception(alert_admin=True, subject='EMERGENCY') + WebInterfaceYourSearchesPages = WebInterfaceDumbPages + try: from invenio.bibdocfile_webinterface import bibdocfile_legacy_getfile except: @@ -345,6 +351,7 @@ class WebInterfaceInvenio(WebInterfaceSearchInterfacePages): 'yourcomments', 'ill', 'yourgroups', + 'yoursearches', 'yourtickets', 'comments', 'error', @@ -387,6 +394,7 @@ def __init__(self): yourloans = WebInterfaceDisabledPages() ill = WebInterfaceDisabledPages() yourgroups = WebInterfaceDisabledPages() + yoursearches = WebInterfaceDisabledPages() yourtickets = WebInterfaceDisabledPages() comments = WebInterfaceDisabledPages() error = WebInterfaceErrorPages() @@ -419,6 +427,7 @@ def __init__(self): yourloans = WebInterfaceYourLoansPages() ill = WebInterfaceILLPages() yourgroups = WebInterfaceYourGroupsPages() + yoursearches = WebInterfaceYourSearchesPages() yourtickets = WebInterfaceYourTicketsPages() comments = WebInterfaceCommentsPages() error = WebInterfaceErrorPages() @@ -443,7 +452,6 @@ def __init__(self): goto = WebInterfaceGotoPages() authorlist = WebInterfaceAuthorlistPages() - # This creates the 'handler' function, which will be invoked directly # by mod_python. invenio_handler = create_handler(WebInterfaceInvenio()) From fc9a1746042e0c4a0d38ee87af5935388d663a53 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Mon, 25 Jul 2011 14:18:27 +0200 Subject: [PATCH 52/83] WebSearch: yoursearches interface improvements * Redesigns the "Your Searches" page. * Adds the "Search into your searches" functionality. * Fixes a few small bugs. (closes #881) --- modules/webalert/lib/webalert.py | 23 ++ modules/websearch/lib/websearch_templates.py | 353 +++++++++++++++--- .../websearch/lib/websearch_webinterface.py | 17 +- .../websearch/lib/websearch_yoursearches.py | 104 +++++- modules/webstyle/css/invenio.css | 88 +++++ modules/webstyle/img/yoursearches_alert.png | Bin 0 -> 991 bytes .../webstyle/img/yoursearches_alert_edit.png | Bin 0 -> 1213 bytes .../webstyle/img/yoursearches_first_page.png | Bin 0 -> 1568 bytes .../webstyle/img/yoursearches_last_page.png | Bin 0 -> 1548 bytes .../webstyle/img/yoursearches_next_page.png | Bin 0 -> 797 bytes .../img/yoursearches_previous_page.png | Bin 0 -> 805 bytes modules/webstyle/img/yoursearches_search.png | Bin 0 -> 803 bytes .../yoursearches_search_last_performed.png | Bin 0 -> 661 bytes 13 files changed, 499 insertions(+), 86 deletions(-) create mode 100644 modules/webstyle/img/yoursearches_alert.png create mode 100644 modules/webstyle/img/yoursearches_alert_edit.png create mode 100644 modules/webstyle/img/yoursearches_first_page.png create mode 100644 modules/webstyle/img/yoursearches_last_page.png create mode 100644 modules/webstyle/img/yoursearches_next_page.png create mode 100644 modules/webstyle/img/yoursearches_previous_page.png create mode 100644 modules/webstyle/img/yoursearches_search.png create mode 100644 modules/webstyle/img/yoursearches_search_last_performed.png diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 3b51db2f50..0898ae0a93 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -385,3 +385,26 @@ def perform_request_youralerts_popular(ln=CFG_SITE_LANG): return webalert_templates.tmpl_youralerts_popular(ln = ln, search_queries = search_queries) + +def count_user_alerts_for_given_query(id_user, + id_query): + """ + Count the alerts the user has defined based on a specific query. + + @param user_id: The user id. + @type user_id: integer + + @param user_id: The query id. + @type user_id: integer + + @return: The number of alerts. + """ + + query = """ SELECT COUNT(id_query) + FROM user_query_basket AS uqb + WHERE id_user=%s + AND id_query=%s""" + params = (id_user, id_query) + result = run_sql(query, params) + + return result[0][0] diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 635c576fa3..3bcc9f1771 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -27,6 +27,7 @@ import re import locale from urllib import quote, urlencode +from urlparse import parse_qs from xml.sax.saxutils import escape as xml_escape from invenio.config import \ @@ -66,6 +67,7 @@ CFG_HEPDATA_PLOTSIZE, \ CFG_BASE_URL, \ CFG_SITE_URL, \ + CFG_SITE_SECURE_URL, \ CFG_WEBSEARCH_PREV_NEXT_HIT_FOR_GUESTS from invenio.search_engine_config import CFG_WEBSEARCH_RESULTS_OVERVIEW_MAX_COLLS_TO_PRINT @@ -92,6 +94,11 @@ from invenio import hepdatadisplayutils +from invenio.dateutils import convert_datetext_to_datestruct +from datetime import date as datetime_date + +#from invenio.websearch_yoursearches import count_user_alerts_for_given_query + _RE_PUNCTUATION = re.compile(CFG_BIBINDEX_CHARS_PUNCTUATION) _RE_SPACES = re.compile(r"\s+") @@ -5082,88 +5089,310 @@ def restore_search_args_to_default(self, arg_list): return default_args def tmpl_yoursearches_display(self, - ln, nb_queries_total, nb_queries_distinct, search_queries, + page, + step, + paging_navigation, + p, guest, - guesttxt): + guesttxt, + ln=CFG_SITE_LANG): """ - Display the user's search history. - - Parameters: + Template for the display the user's search history. - - 'ln' *string* - The language to display the interface in - - - 'nb_queries_total' *string* - The number of personal queries in the last period + @param ln: The language to display the interface in + @type ln: string - - 'nb_queries_distinct' *string* - The number of distinct queries in the last period + @param nb_queries_total: The number of the total user search queries + @type nb_queries_total: integer - - 'search_queries' *array* - The existing queries: + @param nb_queries_total: The number of the distinct user search queries + @type nb_queries_total: integer - - 'id' *string* - The id of the associated query + @param search_queries: The actual user search queries + @type search_queries: dictionary + - 'id' *string* - The id of the associated query + - 'args' *string* - The query string + - 'lastrun' *string* - The last running date - - 'args' *string* - The query string + @param page: The number of the page the user is on + @type page: integer + + @param step: The number of searches to display per page + @type step: integer - - 'textargs' *string* - The textual description of the query string + @param paging_navigation: Four element tuple containing the display + information about the paging navigation arrows + @type paging navigation: tuple - - 'lastrun' *string* - The last running date (only for personal queries) + @param p: Pattern for searching inside the user searches + @type p: string - - 'guest' *bool* - If the user is a guest user + @param guest: Whether the user is a guest or not + @type guest: boolean - - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) + @param guesttxt: The HTML content of the warning box for guest users + (produced by webaccount.tmpl_warning_guest_user) + @type guesttxt: string """ - # load the right message language + # Load the right language _ = gettext_set_language(ln) + # In case the user has not yet performed any searches display only the + # following message if not search_queries: - out = _("You have not executed any search yet. Please go to the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ - {'x_url_open': '<a href="' + CFG_SITE_URL + '/?ln=' + ln +'">', - 'x_url_close': '</a>'} + if p: + out = _("You have not searched for anything yet including the terms %(p)s. You may perform that %(x_url_open)ssearch%(x_url_close)s for the first time now.") % \ + {'p': '<strong>' + cgi.escape(p) + '</strong>', + 'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/search?ln=' + ln + '&amp;p=' + cgi.escape(p) + '">', + 'x_url_close': '</a>'} + else: + out = _("You have not searched for anything yet. You may want to start by the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ + {'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/?ln=' + ln +'">', + 'x_url_close': '</a>'} return out - out = '' - - # display message: number of items in the list - msg = _("You have performed %(x_nb1)s searches (%(x_nb2)s different questions) during the last 30 days or so.") % {'x_nb1': nb_queries_total, - 'x_nb2': nb_queries_distinct} - out += '<p>' + msg + '</p>' - - # display the list of searches - out += """<table class="alrtTable"> - <tr class="pageboxlefttop"> - <td style="font-weight: bold">%(no)s</td> - <td style="font-weight: bold">%(question)s</td> - <td style="font-weight: bold">%(action)s</td>""" % { - 'no' : "#", - 'question' : _("Question"), - 'action' : _("Action") - } - out += '<td style="font-weight: bold">%s</td>' % _("Last Run") - out += "</tr>\n" - i = 0 - for search_query in search_queries : - i += 1 - # id, pattern, base, search url and search set alert, date - out += """<tr> - <td style="font-style: italic;">#%(index)d</td> - <td>%(textargs)s</td> - <td><a href="%(siteurl)s/search?%(args)s">%(execute_query)s</a><br /> - <a href="%(siteurl)s/youralerts/input?ln=%(ln)s&amp;idq=%(id)d">%(set_alert)s</a></td>""" % { - 'index' : i, - 'textargs' : search_query['textargs'], - 'siteurl' : CFG_SITE_URL, - 'args' : cgi.escape(search_query['args']), - 'id' : search_query['id'], - 'ln': ln, - 'execute_query' : _("Execute search"), - 'set_alert' : _("Set new alert") - } - out += '<td>%s</td>' % search_query['lastrun'] - out += """</tr>\n""" - out += "</table><br />\n" - if guest : - out += guesttxt + # Diplay a message about the number of searches. + if p: + msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches including the term %(p)s.") % \ + {'searches_distinct': nb_queries_distinct, + 'searches_total': nb_queries_total, + 'p': '<strong>' + cgi.escape(p) + '</strong>'} + else: + msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches.") % \ + {'searches_distinct': nb_queries_distinct, + 'searches_total': nb_queries_total} + out = '<p>' + msg + '</p>' + + # Search form + search_form = """ + <form name="yoursearches_search" action="%(action)s" method="get"> + <small><strong>%(search_text)s</strong></small> + <input name="p" value="%(p)s" type="text" /> + <input class="formbutton" type="submit" value="%(submit_label)s" /> + </form> + """ % {'search_text': _('Search inside your searches for'), + 'action': '%s/yoursearches/display?ln=%s' % (CFG_SITE_SECURE_URL, ln), + 'p': cgi.escape(p), + 'submit_label': _('Search')} + out += '<p>' + search_form + '</p>' + + counter = (page - 1) * step + yoursearches = "" + for search_query in search_queries: + counter += 1 + search_query_args = search_query['args'] + search_query_id = search_query['id'] + search_query_lastrun = search_query['lastrun'] + search_query_number_of_user_alerts = search_query['user_alerts'] + + search_query_details = get_html_user_friendly_search_query_args(search_query_args, ln) + + search_query_last_performed = get_html_user_friendly_search_query_lastrun(search_query_lastrun, ln) + + search_query_options_search = """<a href="%s/search?%s"><img src="%s/img/yoursearches_search.png" />%s</a>""" % \ + (CFG_SITE_SECURE_URL, cgi.escape(search_query_args), CFG_SITE_URL, _('Search again')) + + search_query_options_alert = search_query_number_of_user_alerts and \ + """<a href="%s/youralerts/display?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert_edit.png" />%s</a>""" % \ + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Edit your existing alert(s)')) + \ + '&nbsp;&nbsp;&nbsp;' + \ + """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up a new alert')) or \ + """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up a new alert')) + + search_query_options = "%s&nbsp;&nbsp;&nbsp;%s" % \ + (search_query_options_search, \ + search_query_options_alert) + + yoursearches += """ + <tr> + <td class="websearch_yoursearches_table_counter"> + %(counter)i. + </td> + <td class="websearch_yoursearches_table_content" onMouseOver='this.className="websearch_yoursearches_table_content_mouseover"' onMouseOut='this.className="websearch_yoursearches_table_content"'> + <div>%(search_query_details)s</div> + <div class="websearch_yoursearches_table_content_options">%(search_query_last_performed)s</div> + <div class="websearch_yoursearches_table_content_options">%(search_query_options)s</div> + </td> + </tr>""" % {'counter': counter, + 'search_query_details': search_query_details, + 'search_query_last_performed': search_query_last_performed, + 'search_query_options': search_query_options} + + footer = '' + if paging_navigation[0]: + footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, 1, step, cgi.escape(p), ln, '/img/yoursearches_first_page.png') + if paging_navigation[1]: + footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, page - 1, step, cgi.escape(p), ln, '/img/yoursearches_previous_page.png') + footer += "&nbsp;" + displayed_searches_from = ((page - 1) * step) + 1 + displayed_searches_to = paging_navigation[2] and (page * step) or nb_queries_distinct + footer += _('Displaying searches <strong>%i to %i</strong> from <strong>%i</strong> total unique searches') % \ + (displayed_searches_from, displayed_searches_to, nb_queries_distinct) + footer += "&nbsp;" + if paging_navigation[2]: + footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, page + 1, step, cgi.escape(p), ln, '/img/yoursearches_next_page.png') + if paging_navigation[3]: + footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, paging_navigation[3], step, cgi.escape(p), ln, '/img/yoursearches_last_page.png') + + out += """ +<table class="websearch_yoursearches_table" cellspacing="2px"> + <thead class="websearch_yoursearches_table_header"> + <tr> + <td colspan="2"></td> + </tr> + </thead> + <tfoot class="websearch_yoursearches_table_footer"> + <tr> + <td colspan="2">%(footer)s</td> + </tr> + </tfoot> + <tbody> + %(yoursearches)s + </tbody> +</table>""" % {'header': _('Search details'), + 'footer': footer, + 'yoursearches': yoursearches} return out + +def get_html_user_friendly_search_query_args(args, + ln=CFG_SITE_LANG): + """ + Internal function. + Returns an HTML formatted user friendly description of a search query's + arguments. + + @param args: The search query arguments as they apear in the search URL + @type args: string + + @param ln: The language to display the interface in + @type ln: string + + @return: HTML formatted user friendly description of a search query's + arguments + """ + + # Load the right language + _ = gettext_set_language(ln) + + # Arguments dictionary + dict = parse_qs(args) + + if not dict.has_key('p') and not dict.has_key('p1') and not dict.has_key('p2') and not dict.has_key('p3'): + search_patterns_html = _('Search for everything') + else: + search_patterns_html = _('Search for') + ' ' + if dict.has_key('p'): + search_patterns_html += '<strong>' + cgi.escape(dict['p'][0]) + '</strong>' + if dict.has_key('f'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f'][0]) + '</strong>' + if dict.has_key('p1'): + if dict.has_key('p'): + search_patterns_html += ' ' + _('and') + ' ' + search_patterns_html += '<strong>' + cgi.escape(dict['p1'][0]) + '</strong>' + if dict.has_key('f1'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f1'][0]) + '</strong>' + if dict.has_key('p2'): + if dict.has_key('p') or dict.has_key('p1'): + if dict.has_key('op1'): + search_patterns_html += ' %s ' % (dict['op1'][0] == 'a' and _('and') or \ + dict['op1'][0] == 'o' and _('or') or \ + dict['op1'][0] == 'n' and _('and not') or + ', ',) + search_patterns_html += '<strong>' + cgi.escape(dict['p2'][0]) + '</strong>' + if dict.has_key('f2'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f2'][0]) + '</strong>' + if dict.has_key('p3'): + if dict.has_key('p') or dict.has_key('p1') or dict.has_key('p2'): + if dict.has_key('op2'): + search_patterns_html += ' %s ' % (dict['op2'][0] == 'a' and _('and') or \ + dict['op2'][0] == 'o' and _('or') or \ + dict['op2'][0] == 'n' and _('and not') or + ', ',) + search_patterns_html += '<strong>' + cgi.escape(dict['p3'][0]) + '</strong>' + if dict.has_key('f3'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f3'][0]) + '</strong>' + + if not dict.has_key('c') and not dict.has_key('cc'): + collections_html = _('in all the collections') + else: + collections_html = _('in the following collection(s)') + ': ' + if dict.has_key('c'): + collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in dict['c']) + elif dict.has_key('cc'): + collections_html += '<strong>' + cgi.escape(dict['cc'][0]) + '</strong>' + + search_query_args_html = search_patterns_html + '<br />' + collections_html + + return search_query_args_html + + +def get_html_user_friendly_search_query_lastrun(lastrun, + ln=CFG_SITE_LANG): + """ + Internal function. + Returns an HTML formatted user friendly description of a search query's + last run date. + + @param lastrun: The search query last run date in the following format: + '2005-11-16 15:11:57' + @type lastrun: string + + @param ln: The language to display the interface in + @type ln: string + + @return: HTML formatted user friendly description of a search query's + last run date + """ + + # Load the right language + _ = gettext_set_language(ln) + + # Calculate how many days old the search query is base on the lastrun date + # and today + lastrun_datestruct = convert_datetext_to_datestruct(lastrun) + today = datetime_date.today() + if lastrun_datestruct.tm_year != 0 and \ + lastrun_datestruct.tm_mon != 0 and \ + lastrun_datestruct.tm_mday != 0: + days_old = (today - datetime_date(lastrun_datestruct.tm_year, + lastrun_datestruct.tm_mon, + lastrun_datestruct.tm_mday)).days + if days_old == 0: + out = _('Today') + elif days_old < 7: + out = str(days_old) + ' ' + _('day(s) ago') + elif days_old == 7: + out = _('A week ago') + elif days_old < 14: + out = _('More than a week ago') + elif days_old == 14: + out = _('Two weeks ago') + elif days_old < 30: + out = _('More than two weeks ago') + elif days_old == 30: + out = _('A month ago') + elif days_old < 180: + out = _('More than a month ago') + elif days_old < 365: + out = _('More than six months ago') + else: + out = _('More than a year ago') + out += '<span style="color: gray;">' + \ + '&nbsp;' + _('on') + '&nbsp;' + lastrun.split()[0] + \ + '&nbsp;' + _('at') + '&nbsp;' + lastrun.split()[1] + \ + '</span>' + else: + out = _('Unknown') + + return out diff --git a/modules/websearch/lib/websearch_webinterface.py b/modules/websearch/lib/websearch_webinterface.py index 9c2bfdee12..9f779e46bc 100644 --- a/modules/websearch/lib/websearch_webinterface.py +++ b/modules/websearch/lib/websearch_webinterface.py @@ -1212,14 +1212,17 @@ class WebInterfaceYourSearchesPages(WebInterfaceDirectory): def index(self, req, form): """ """ - redirect_to_url(req, '%s/yoursearches/display' % CFG_SITE_URL) + redirect_to_url(req, '%s/yoursearches/display' % CFG_SITE_SECURE_URL) def display(self, req, form): """ Display the user's search latest history. """ - argd = wash_urlargd(form, {'ln': (str, "en")}) + argd = wash_urlargd(form, {'ln': (str, 'en'), + 'page': (int, 1), + 'step': (int, 20), + 'p': (str, '')}) uid = getUid(req) @@ -1228,14 +1231,14 @@ def display(self, req, form): if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: return page_not_authorized(req, "%s/yoursearches/display" % \ - (CFG_SITE_URL,), + (CFG_SITE_SECURE_URL,), navmenuid="yoursearches") elif uid == -1 or isGuestUser(uid): return redirect_to_url(req, "%s/youraccount/login%s" % \ (CFG_SITE_SECURE_URL, make_canonical_urlargd( {'referer' : "%s/yoursearches/display%s" % ( - CFG_SITE_URL, + CFG_SITE_SECURE_URL, make_canonical_urlargd(argd, {})), 'ln' : argd['ln']}, {}) @@ -1259,7 +1262,11 @@ def display(self, req, form): suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") return page(title=_("Your Searches"), - body=perform_request_yoursearches_display(uid, ln=argd['ln']), + body=perform_request_yoursearches_display(uid, + page=argd['page'], + step=argd['step'], + p=argd['p'], + ln=argd['ln']), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { 'sitesecureurl' : CFG_SITE_SECURE_URL, 'ln': argd['ln'], diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index e655c3dff5..1da6fbfac6 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -19,51 +19,113 @@ __revision__ = "$Id$" -from invenio.config import CFG_SITE_LANG, CFG_SITE_URL +from invenio.config import CFG_SITE_LANG, CFG_SITE_SECURE_URL from invenio.dbquery import run_sql from invenio.webaccount import warning_guest_user -from invenio.webalert import get_textual_query_info_from_urlargs from invenio.messages import gettext_set_language from invenio.webuser import isGuestUser +from urllib import quote, quote_plus, unquote_plus +from invenio.webalert import count_user_alerts_for_given_query import invenio.template websearch_templates = invenio.template.load('websearch') +CFG_WEBSEARCH_YOURSEARCHES_MAX_NUMBER_OF_DISPLAYED_SEARCHES = 20 + def perform_request_yoursearches_display(uid, + page=1, + step=CFG_WEBSEARCH_YOURSEARCHES_MAX_NUMBER_OF_DISPLAYED_SEARCHES, + p='', ln=CFG_SITE_LANG): """ Display the user's search history. + @param uid: the user id @type uid: integer + + @param page: + @type page: integer + + @param step: + @type step: integer + + @param p: + @type p: strgin + + @param ln: + @type ln: string + @return: A list of searches queries in formatted html. """ - # load the right language + # Load the right language _ = gettext_set_language(ln) - # firstly, calculate the number of total and distinct queries + search_clause = "" + if p: + p_stripped = p.strip() + p_stripped_args = p.split() + sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg) + '%%\'' for p_stripped_arg in p_stripped_args] + for sql_p_stripped_arg in sql_p_stripped_args: + search_clause += """ AND q.urlargs LIKE %s""" % (sql_p_stripped_arg,) + + # Calculate the number of total and distinct queries nb_queries_total = 0 nb_queries_distinct = 0 - query_nb_queries = """ SELECT COUNT(*), - COUNT(DISTINCT(id_query)) - FROM user_query - WHERE id_user=%s""" + query_nb_queries = """ SELECT COUNT(uq.id_query), + COUNT(DISTINCT(uq.id_query)) + FROM user_query AS uq, + query q + WHERE uq.id_user=%%s + AND q.id=uq.id_query + %s""" % (search_clause,) params_nb_queries = (uid,) res_nb_queries = run_sql(query_nb_queries, params_nb_queries) nb_queries_total = res_nb_queries[0][0] nb_queries_distinct = res_nb_queries[0][1] - # secondly, calculate the search queries + # The real page starts counting from 0, i.e. minus 1 from the human page + real_page = page - 1 + # The step needs to be a positive integer + if (step <= 0): + step = CFG_WEBSEARCH_YOURSEARCHES_MAX_NUMBER_OF_DISPLAYED_SEARCHES + # The maximum real page is the integer division of the total number of + # searches and the searches displayed per page + max_real_page = (nb_queries_distinct / step) - (not (nb_queries_distinct % step) and 1 or 0) + # Check if the selected real page exceeds the maximum real page and reset + # if needed + if (real_page >= max_real_page): + #if ((nb_queries_distinct % step) != 0): + # real_page = max_real_page + #else: + # real_page = max_real_page - 1 + real_page = max_real_page + page = real_page + 1 + elif (real_page < 0): + real_page = 0 + page = 1 + # Calculate the start value for the SQL LIMIT constraint + limit_start = real_page * step + # Calculate the display of the paging navigation arrows for the template + paging_navigation = (real_page >= 2, + real_page >= 1, + real_page <= (max_real_page - 1), + (real_page <= (max_real_page - 2)) and (max_real_page + 1)) + + + # Calculate the user search queries query = """ SELECT DISTINCT(q.id), q.urlargs, - DATE_FORMAT(MAX(uq.date),'%%Y-%%m-%%d %%H:%%i:%%s') + DATE_FORMAT(MAX(uq.date),'%s') FROM query q, user_query uq - WHERE uq.id_user=%s + WHERE uq.id_user=%%s AND uq.id_query=q.id + %s GROUP BY uq.id_query - ORDER BY q.id DESC""" - params = (uid,) + ORDER BY MAX(uq.date) DESC + LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', search_clause,) + params = (uid, limit_start, step) result = run_sql(query, params) search_queries = [] @@ -74,16 +136,20 @@ def perform_request_yoursearches_display(uid, search_query_lastrun = search_query[2] or _("unknown") search_queries.append({'id' : search_query_id, 'args' : search_query_args, - 'textargs' : get_textual_query_info_from_urlargs(search_query_args, ln=ln), - 'lastrun' : search_query_lastrun}) + 'lastrun' : search_query_lastrun, + 'user_alerts' : count_user_alerts_for_given_query(uid, search_query_id)}) return websearch_templates.tmpl_yoursearches_display( - ln = ln, nb_queries_total = nb_queries_total, nb_queries_distinct = nb_queries_distinct, search_queries = search_queries, + page=page, + step=step, + paging_navigation=paging_navigation, + p=p, guest = isGuestUser(uid), - guesttxt = warning_guest_user(type="searches", ln=ln)) + guesttxt = warning_guest_user(type="searches", ln=ln), + ln = ln) def account_list_searches(uid, ln=CFG_SITE_LANG): @@ -109,7 +175,7 @@ def account_list_searches(uid, out = _("You have made %(x_nb)s queries. A %(x_url_open)sdetailed list%(x_url_close)s is available with a possibility to (a) view search results and (b) subscribe to an automatic email alerting service for these queries.") % \ {'x_nb': nb_queries_total, - 'x_url_open': '<a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_URL, ln), + 'x_url_open': '<a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), 'x_url_close': '</a>'} - return out \ No newline at end of file + return out diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index 288e75eaa2..b8753925a5 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2568,6 +2568,7 @@ a:hover.bibMergeImgClickable img { background:Yellow; } /* end of BibMerge module */ + /* WebAlert module */ .alrtTable{ border: 1px solid black; @@ -2581,6 +2582,93 @@ a:hover.bibMergeImgClickable img { } /* end of WebAlert module */ +/* WebSearch module - YourSearches */ +.websearch_yoursearches_table { + border: 1px solid #bbbbbb; +} +.websearch_yoursearches_table_header td { +/* background-color: #63a5cd;*/ + background-color: #7a9bdd; + color: white; + padding: 2px; + font-weight: bold; + font-size: 75%; + text-transform: uppercase; +/* border-bottom: 1px solid #327aa5; + border-right: 1px solid #327aa5;*/ + border-bottom: 1px solid #3366cc; + border-right: 1px solid #3366cc; + white-space: nowrap; + vertical-align: top; +} +.websearch_yoursearches_table_footer td { +/* background-color: #63a5cd;*/ + background-color: #7a9bdd; + color: white; + padding: 5px; + font-size: 80%; + text-align: center; +/* border-bottom: 1px solid #327aa5; + border-right: 1px solid #327aa5;*/ + border-bottom: 1px solid #3366cc; + border-right: 1px solid #3366cc; + white-space: nowrap; +} +.websearch_yoursearches_table_footer img { + border: none; + margin: 0px 3px; + vertical-align: bottom; +} +.websearch_yoursearches_footer a, .websearch_yoursearches_footer a:link, .websearch_yoursearches_footer a:visited, .websearch_yoursearches_footer a:active { + text-decoration: none; + color: #000; +} +.websearch_yoursearches_footer a:hover { + text-decoration: underline; + color: #000; +} +.websearch_yoursearches_footer img { + border: none; + vertical-align: bottom; +} +.websearch_yoursearches_table_content { + background-color: #f2f2fa; + padding: 5px; + text-align: left; + vertical-align: top; +} +.websearch_yoursearches_table_content_mouseover { + background-color: #ebebfa; + padding: 5px; + text-align: left; + vertical-align: top; +} +.websearch_yoursearches_table_counter { + background-color: #f2f2fa; + padding: 5px 2px; + text-align: right; + vertical-align: top; + font-size: 80%; + color: grey; +} +.websearch_yoursearches_table_content_options { + margin: 5px; + font-size: 80%; +} +.websearch_yoursearches_table_content_options img{ + vertical-align: middle; + margin-right: 3px; +} +.websearch_yoursearches_table_content_options a, .websearch_yoursearches_table_content_options a:link, .websearch_yoursearches_table_content_options a:visited, .websearch_yoursearches_table_content_options a:active { + text-decoration: underline; + color: #333; +} +.websearch_yoursearches_table_content_options a:hover { + text-decoration: underline; + color: #000; +} +/* end of WebSearches module - YourSearches */ + /* BibClassify module */ .bibclassify { text-align: center; diff --git a/modules/webstyle/img/yoursearches_alert.png b/modules/webstyle/img/yoursearches_alert.png new file mode 100644 index 0000000000000000000000000000000000000000..c036ff6de4ff10cfb3b89b0c52740898b78639ee GIT binary patch literal 991 zcmV<510ei~P)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog5HYCq_9^U`}16)Z&K~y-)os?flWOWqBKlhF^G-{&`W4dmuumeR;z8Ds=QZGry zR^ZYSNwg1vU=*=0u}B0N^blDfXh|uqe|B^R1+f>w5Rx83Uv#ml(IHCMomsEWAF^>~ z?zaaWadt<DzHs6G?z!iC&iQ^1KVx`!c-iaqUI8Ew2xKKme_WL@Gc%I~*x%pZB$-S~ zlC+6^`dcItQ8*lyB#n-arphvz%>7U(R1e^AIE<%`o;KfUq0*R`n8><ZE($n0I-<L~ z+leHttgJ|qR##W`;muD3fNk3<;QE_4q)i08Z3KhCT!E&g>YAge7Iv+Z=3}v#78e&) z3JVJhipS#`85v0xPb=y0@KCv2PN7gph0@l>D^m1D{r&?wO|UGhB>NL}m!(ZkABV|~ z!TIR3M)<VG4LE@#*w275G&Gd+`FxU8GRrhgVzHQMnx?a-r^iN;W@l$*S(eU9CP|iM zDHse&k_HC{YXHoAK2KLy7v<&UOioUoGEYrSQCV5Zy;zFdlOf!0_i-SX%Sn=K+g3at zS0obA&d!eZ_V)Dp%g;#JHR)I6t>UYqgk&=LNK1E@o|iU`75cvaSXLvS&nutL<8(S7 zop{@Te@@;X2crq2Y<_;;E+x_I>>bCQJ~*)pCBa+8m*@KMc`v&8ER>l*6Xc@M{}Wkb zdU`tB(9lp84u|RJ=%BH&5!<#o&zFbca5&iB-e!4unU<Co*4Nk5nM|g!x3_m6pv*K) z+S=M^Z*NBu(P)&Vr6ro1n`v!rEh0Nxbai!=t*tFOJ3I0F{rLTU0M^#likk}r0@;ES z1r*#!r_&lAA6F<8QZkv;+}xbT#>SLLBqT|@ySutjSE10?WlH|y)>uEPsjf)B`r5Vn zk_8IHnM_8pSgh#V!^OvMPwBG{Ov(OR+VY;RgS~;SzQE}S=2;0+sT2nX2e@1=d_Erl zhEeQm;rz|FWAwZsxLXc+3yeY>hCc=%1Yk-s5{U%aY!;8lLv?j^N#e7(cmDx(?w|2q z5T#b3;S#+67B2ypjDdlH3ez;}|I5aBJf8eVxB74OUEhB9y(^$YFtR`%coL`wemlM@ zC`VuXh+**m@Z9A_Zulzbto3m5VlBpFE;!7<ZXCr^{PiHsV)ReG`xmaLi`nT|P!9kA N002ovPDHLkV1j_J%S-?O literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_alert_edit.png b/modules/webstyle/img/yoursearches_alert_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..9af0b3632d0233a719efa1871529f32ccee27e5e GIT binary patch literal 1213 zcmV;u1Va0XP)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xop73<hGKhmimP1UgAXK~y-)jgx;&)O8rgU!U)N?~q>#=V1(qz|5JoNt2=^DCe9T zbFQSJHC^RGnfPB@vp@Qy%|Bdi*jk1g%jJ;li`}`{a8vXgGo7f1fWHQGf_kE!-W>-V z-mmXH{RLAa*Xz&E_PjsO`~5tR+P{Cl+itg40$><MLPTU}@VcnBwzdR7C=|M?#>dCS zX0y@S`rD*33?ornTdRA$UQE-(W=o^<<f)_{i^U!|oz8p!mnUj@Fcvc#>sM&%(4j*K zo6W{F@pzogn>Sk#5$Wpc5)qN^^W7pMa<>1bkV>Uwn!%v~IdS@&0O&54%bb=OdR(Zz zl0KTccI}#A7)HYH_e)1dhdhhVcSqXW|CEOhA4+p`^ThfOKDX)sd3kwMS635_MzLD0 zw70iYT3X6vWP;MtH*h!{*z=3C)@`uw!Z3_PFc_5CXgl5~pYAy+SFZMp&*u}KL)wSm zbjZ4|jtY<uWB>*3Q>P|XQ&UsY;c$q^>;jsm;rIJBP1CIP_4O%4M2;LeBEeu#o{Y>i zO&J*(7N^rGA|g#c@5|Y+ahru?GAXH4N}|!Iw6wI$6tuRs%7u&F(to8-L`04rKRed$ zz9=f0Op3)~K|}}y0))e1B2y7GO(P>cgHR|)L18iOzb<$N2Zt+W3MM8d$jZt>DFwj2 z`=cZiNutpt1KwNQxq02bqhV)AnWibGX=1fn|7CdVcc&QjjdEbuYR+Fcw{K)*c&BnY zo#wL<MU$f4)0QaLu`cRYJIq8pLdD8=EuNl>J7!I??Bm9RmD_$1d+C<XfGlAC_WCcj z9&GuwTOB@pI8jhgpbrcTuy*ZQii(OzrBeLYT%N^ZvEcQ3al74=m6g%c(=!>1#flml z8bScNrfF1ER8Un_g@`abJWOY2C(D;Fr@XxUDY5BKP1EELXB$<O@8i4cqp)Bhb#-+B zcsw2gp-9#N<JgdyNM6#P6i`Z0Qc^-)T^&j(B9RC#my68IOxCPfgZs1_zyCfT)zq?S z<0tg?^-xk=!lS?=3Rnn0O6i%U@4R<AWtxoo?sh7}FcLL2HTv{!D5Z$U<AlRu^78U# z-T-2;7=L?*cws?47TrQ3o**0!lbxMSPw!=HbL@n|V^cksE*I_D{dE99d(y$g!~|nw zW7upq91h2ftC@>ROJmuqui<bw$j!;+>g@<Wxq4|o-HR<fm!*pfF{445qerFjhwlN< zMMO<^F?a6Vr{bCor4+xP=;zFh5Yfkz4BsBd)aS8&)k_2d0m|QcJ7dL)Hv_$Wo<`tH zwRi8{Oik1BpSLMWDawk}<ZgHN$?cm~z3#bjht4yXB{SQ8Yf<`x>bI9<#HON2blobE zsc`Yim6gZF!{ccdT~qu|EWrG<oT8=WTYl_X)zBi#)_>a!ybQbu<N)cwJRlEPuz2yJ b5+L^<z1V3TXNh(`00000NkvXXu0mjfOI}3r literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_first_page.png b/modules/webstyle/img/yoursearches_first_page.png new file mode 100644 index 0000000000000000000000000000000000000000..a16d039aa93a3513f8d2116cf15d017f3cbe85ad GIT binary patch literal 1568 zcmV+*2H*LKP)<h;3K|Lk000e1NJLTq0012T000mO1^@s7oeD%p00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85Fvhu*DU}51)WJmK~y-)eN<^|R8<%~_s*L)GktHmJUT7&W;#<4DU_BjOqVHO z2U06o5Uq#>QILcfP+W1LkXEA-gw{}sh(r-lt5%H27Em;jAh-~gYH7iUG*r6pLpz=2 zef}twLWt+r&G~Zg{c_IvKE%Yv!~+25JO%*x)T!>o*7g>9Y{}w1z1|)|Qc~hQxDHE< z#mOB``++`hFSWL{XftPel8~9{n&ZyNx{{OQ&S~#x)v~g)5;8JeDV^ONl$w?@m*4-u zyg;@)gHD_{dBQ&!V5Oy{gTMWD>RfkMx3+N6!+toNafP@6{fM&;E$vMN0NZzLr}mCk z0)S0r8>y|mg;<}rmjb~67-Qg!BNz;V>o^z~_@I9fTQ*m+k3QV#A<{5PiDuQGeK8pz zATe>mea$WZP_{es%837%JIDQ$&o@9eyKMphsHzG8NJ>sx+}zwm<Hn7hCs<WAWHL$M zobhL$Th&oswpr;PxCSDEZ_o#Kw%c!sQU0#nvMt_wtq%Yp5DY-I*+P8c#DtI}$#6Iw zp=C>-a8&R4yh$$z5JdwdNk(2?9=%>#maucz`<IOd1B?a}A|n(`nUWU5$5p%XTUwfF zWBK~V5p9Er_||tTzOApXa}0Burh$l9#n$pqKCk{XA7k$rQyAy47XlD)3lb)bzYDj) zm@&>Q)YQ}v5fK1BT!?en<4Q```KqcurI8mDB`W&Gmsa<juRBiyFjP~c@1*q1$vkS5 z@xbogpUm#)YQv#}hXDXNZV$7n%6ohEe6_rzs|{Bhn;_8jHxpAG1)IuW-*2<2)aUJm zUJwBgJf7VAl9F{t($dqYr?(rN>4yBPsi~pl<Yb&VbK2*KcNiIC&<g?phNDN1LQxcN za!Rrc!hd|@l}2#AIHXBhTDqxt&C6vB96Bygm`Q@cAi|S7!(3dv=IKySgN_SiGKE3V z>w!y`{u&l%*a~dlvGw<UUqAhL?1zy&D}Q^#sR3U<U2gcBgg$RCy1F`VWREcbNOrlh z)b`G{e@5^Gz_obs<B@gswG9|Qe%!qM`}a|OeH}G4G|<#(?yDoMxeavSe9W}z?qg4_ zT1k>DCF1|V0K`slrG8oZYKb=?;m&zHOp-LGGagY&BpeP0G))TufXCyx>7&d{*Q2YR zUU})l#rj}Rcju+Od%w9WD>EDM&iK&n8cRz}xwmNHLl@4TJrnBb?P@-9<lFfV7S5M+ zf<S^S%S0gP_4?~l0B^kZ7CZUN@yLvf^vx?)J~_9bV2)yzW%vVr8q!&VPA7msh>NpR zZEY>)7cMk5G+rzzDq6U#sHiA5Dk=(ozn=gwB7p0300^=yYl1Ax8i*hoi~s<m$z=89 z<@7)E!U}QF?}HZ7z&XQ!um8F^1ZV_}2nK@@=5X@|2M+9WO`ks97zhOr2n2|84)3)- zFvb|b03w965IE;h6oq(9jD-Y2fL=EgLzBtWTvhd1er4sBKbxCc5Up6C)9FYr4s8TE z%#3hz1j5bXFc|d6%Fce|>%Cv+Ra92|T66Aq#9FLO6h$%`Ob9bcFw5bH2#-WWcoYOd zfNCAB33j`UL{UT_7=S1mV6~3cw!O3M7=ZD`Yt~`?hBqe~jpEBrNBok)2j=72fR~_w zan2!065?!jzWVvqKLN-hB5W$#7_U)qx!q<jeq_;O7#QqFAP@p)9C}d?dz_sF)uw8q z!GO;0c1U3|Mq6WVN<{H1YwG|kC8DMK4t=AvHebzGl;|z9XBAjvvy5<a1dSY->gwvt z3l=O`OGIl={``}4?%e5IizT)qH+RN3lhFjLY9)5Q?vK&6=l@6zX(1vaL`Fsq%(}ba zIAct^HIlWpwU{(%(tmsP@OKAYDM{%UR8{S{-K-GN@a%$!@ZI-^Cy!Drt^WevHBK#s Snf#mp0000<MNUMnLSTa2;n+I> literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_last_page.png b/modules/webstyle/img/yoursearches_last_page.png new file mode 100644 index 0000000000000000000000000000000000000000..c61494c785551a39a17c19ea3dcd458e53aeec0a GIT binary patch literal 1548 zcmV+n2J`ueP)<h;3K|Lk000e1NJLTq0012T000mO1^@s7oeD%p00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85D+g`*B1Z)1&K*SK~y-)g;Q;8Q)Lu>-nVx{-B{UR182g5kq<Wzh-3(&5E0e_ zI*niuK?JA72__nTm}s0ad>CO93Cac<2TJ&u7!`E5P5BbWq7Eo!>pIwM*WT`nj^5te z-uB+U#~&M7NsQ00lPAwP?>WyoFaBu$Z48D`TX+0X*z84%13iAxKhQ^yJwDF_0LZdz z1%Q@T=NR<e=%r=PE<M}Z*G)Ei(IU&cWpCSRe>%}_wOaR;l$4C9-v31~eR%qjarfpm z+iXRx$S=76$xD|m3IHf5D45vhb`b!~nmw~?+VrVUfQW820V3kJ;zKnDilAvKeRW_T z0YD_8(a~>e2mn+qOsu<CCeAsAr3^#Nh{M7;Qd5V4aSrs~=%wk?rY-^o<HEujtLDv{ zho0U5VRzN8qQ3rK+FiMG5fS0=;llucrlt#dk*G#{KmT-ja$oP1z2)`#J89RaA1}n( zwX5w>JxY2^CnX#rAq2g@>Aj}y72C(^u_0bnNF)-pW$PyQ;wS9BSWKrER<E>k=hYTV zRF6Q{qmU#CAML2<aWuEhZK$iiBN~kk^4x;z{nf#9XPXKKf<e+EDmpqkEtoiQeDQ-b zrquvoNJ3hATK=3zW}OCLN#-A(GfQ^4T4}<B`-)N5Q2W4;$H^Mh)*UYd0H@RWuRR<* z@>NmI(L**S%YHiF`0KUY+<T_Yo;63iC2DLsN3!hilTuSB6)%1&n3PQ3E-!Gs;~Irk zg@}kg_;B;)TQAo){6wl6CL*GW9owpt6WiO}1edFo;_(=15tRl)H^eU|f2Nm8);#>} zci#;08V%gMA)HsQQ0Z%>bBTzqUUiaWnkGWZO@IMH2=>6#DTruKH)xS4lyC@)bC3{h z%-y50V#RZH0AQLXxDWy<DXB=2Qpq$Du-oigmn?bm&R9%`BuTJv3ndZ+dy#G5sfP1+ z0RR{S91aJClmS})+_JW0`@j5hKZTT=wBhyAjwGHF5jh+VLfL!ossKjgHmu)JO5l9l zZ8Z0IJap{X_rwkzIk3=bwbr=X+m0=NcIEn<?3@r|jD!&2oZns#E?4VVXRE{0-Q8XP z(9GE{J~(|wH)BlTKk#As7GKGlb@v#Cf$p9F9L~!{+~e`E`zIA-tX{R|`HYO=1D$_% znB^58{JwWj)f6IPw-@Q??Bt`evaxc-ss;J^1^z%lPE_vMb!Eqn?Q@e4u&k^M01%L6 zk(rqVONs@<(=*WJ>0)@}&DZVjb~hQOL569Nu187JRH5q;>h0^LJ)c!}t}A_cLw}Ew z3ILUrmE(T+@faDVL8fVv9@GEU2M2<5@bCfom33>kIW9ZXhzQG9Eb|f(nF*6xT`jbJ z!>cw)mIK^03}~7<XeR?e#NwuaMdEm>`02c7mM+~15#M?0^hve4`iqM>ql*!VXkd(i zF$N;R;-&yWVE+8a?_RKA;YQB+#)}u5btM#%03aHTz=)gh`(-W#0^Q6sO&Dez7A}Ev z27rNqfip(d%#rNOnNz6w`pBQHEp6*Yjmo~Js_ONB|78ZrB7vBo5gD1x?QTQWp4~y` zm6p|!s9FUeR|o+jf}(_(q$tYZ-Utws0LX$&YbLgrZ^MPg3!izt-cM4bbjSI|Qv?7r zADTH4U@%Q%$Q97YtZY`bw+iRZo;`8>ddH@$5xEzR|9Dj7jvCMMCS(Z!(Bn}s280!b zNoqLErIZvTOo8%>a;3KR#A~wOe<Bzh=m&t@++4`A{P#>gpN~lv3)0huW6RdfvHJR- zH~M^C-*tI?zQNZ2f!yED#*Qx>oGBpyFsQ1^S$0l#UV7SHV>&xMzXM3%f517<89(8^ yNo_87BLL%n<kIBH`SU}HlExT=BuP+Jt@$t8`Y@C_t857X0000<MNUMnLSTYrdE+1e literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_next_page.png b/modules/webstyle/img/yoursearches_next_page.png new file mode 100644 index 0000000000000000000000000000000000000000..c12bad413971c3258a23eeba31e611aace9cd6f2 GIT binary patch literal 797 zcmV+&1LFLNP)<h;3K|Lk000e1NJLTq000dD000mO1^@s7x}?Rg00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85HFc9`YHeb0-8xgK~yNujgi|+({UKapZELwI$CCS@Bp<ASjsF)5L*2K9%j@< z7tvKTH3_=urn?S9=wMhBO~Q;$M2ZCkyJ#6jUKFBpYC0*~@4~IM+03oALv26zd%K9l zqM*<1^FBNe?}L%i;ZguV!)~iJ{OKbD03wk{K4#}4+|+CvguV4-UFuts&$pd5JDtu_ zY}s15XL8cZ0AMzoO<?}%0|1dur?~fF4+8+rM8Yft!MJ28q*N5t*VP|jrU&Tkx>gg7 z&2i^-$G*jWuh&~5+=Fh#Xj}@Lt>sZ7`IU7&$LHG4_*_q22I-yjD&M}RAxOYrFyQ8` zo6-IkuXZYujHsyCymyDC+6h2l5mL(ac=p^?1>m>706_l0G`oif1BJ^Ax7XHK;|t2W zskfX+er0Cni<d5REePY|Z#bLHa4MbR*q1qd!aKoD%{D7}JRV-Z!2}RN*K-g=ct54; zQrC0H%QFA~XTIyyuxIz(EnCetxZOh>PkiC#!;LBcWg+9#=@V%HYw-uv`>?yFpkUc! zpXzfpSoa<+-&8(FM2vqB3<iruz#pKB&6Udz9c<jc&SaYX@FAc(IxfF)T)nc3nd#TD z-EIc}sH#db9G(%GOa}S+1pt7O(h@W^wro=rF%XSKalhwY=w<)Q?y9QlRshNA>1mRo zP>3{5Ln@Vo5CTL5W}UM0IzWut+PXFMR_k#=;`qpjXa3IJ+ru&(o*_-spv`BY2nitw z0D(XdM7+FkCHZ_lblvETjg5{US-ontTgKz@pDQz<a}KN%R;?&P$2A8AUJp1`Rc+I< zVrX=1PzL~u#bT0)M1rKugCGU%?d_?)zP@9@VDQOo<a79M-}C$Zgref24P|8;cL9+2 bKUV$#Ae=X4gvQ*!00000NkvXXu0mjf?67Wo literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_previous_page.png b/modules/webstyle/img/yoursearches_previous_page.png new file mode 100644 index 0000000000000000000000000000000000000000..b3bdbd05bec2e215a05b2b710fa070e3604ada6a GIT binary patch literal 805 zcmV+=1KRwFP)<h;3K|Lk000e1NJLTq000dD000mO1^@s7x}?Rg00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85IJq+PkR6W0-{MoK~yNub&xwuQ*ji=zyH0rP#&dF`k+{fMF$W<p@m9<4<rx= zpo>bBO<IG4(Zod^9gIX24Uff$8&QlghCv}p!e&AvMAH%;1tXx8wzmb_0-^LC2b3Ve zZ#p?SkMH{dKo$Vt`i*O!BGF$208nex`Mf{4LetYz!?4+G2Ai$2N(=|6tlZpy(a}*Y z04ywgr;dm1_1Gf;0MOmrMS<W7vCyhWj4?1U@G0`tyWW}UVRM<;37t-tw|BAJQsxlF zARRec&?ua!uIBUd4~04&Jj#l##{dxG+i@5Sh6LASw{M&}S!aCpdf=x#IT><!3Nn={ zSgjU`EiEl+3<g6003=BQV~ln6b`Ip|^iOe+3JMNa{nOBB)%x3aZbc_1CU$6)mmTL? zS}wgMA_|85<Z_J>YHChjn4I)bcvX~E*TNJEi{x^R5gQ->95<Sb0t16g5CAaD&&}`L zcDA*4h#`?;v8WW^j+4jZrrpOx#JYPPf7#evC$D#w*nX=s-Wj#mJN1nh1i>gt(i;9R z4nS_D&C>61wD$Rym%pO0u+RViR;zXIAaAvnH#?k{y&jL7)*`DE6n7#XD=umjOePcA zsx0vR2&pP6EuC#`SDWljc9kMSfvv4A0sxW-9M1y)6pD07(C6yjI9i<*rRGv@b8{Wr zi3GqAqOk}V<Jh0HNJtVmj)zK>EpbIf#pn8-KWpgj>GJw~0q8V2NRlOyJUIoaDQQro zXF!>e31vnmWHJG{dOfkh!9ktfZeRHu=6?IVLYYjsnP)U!X+D1u>zgr12?-qIu(-5{ z7ccwIGPm2UEh#DS??*;N_&74GoSU1iRco|QYR}XeBasN+5522re!o9WtJTI1VogL~ jjP2GRMuyDlY_0D%<{TiF_>Qx&00000NkvXXu0mjfk8WAP literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_search.png b/modules/webstyle/img/yoursearches_search.png new file mode 100644 index 0000000000000000000000000000000000000000..742d87d0eea3ba912475ce8cca9909ff3ccdf13f GIT binary patch literal 803 zcmV+;1Kj+HP)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog5H!>o%n$G|L0-#AmK~y-)osVlsQ*juFfB$oiZo0YcY|5tQWois;n1Q57T4GX$ z%m}>X1#Id>G?bM5qS2Qikj$VP5>i<NMi;Zv%n$^fnMCQxT86g_5yFMLxUI9(hghhn zV9&>w=Y4oz@H`JM<ihO-1JLCaXaN8n^Hx0oAS3%koc7^6L%^M3JvL%}Q~tM*m7A~i z4ry8YV7VtgHM}}X8ayyGY)0d)`;YI=TT1~j>-MWM0umzSh$R4U{7OgrQ=&40fK-bt zSy%Nrx_`~IV_9*Y%tB;DDM>;I3$sx1sX!@+92_=&UY9iJPvumU>2m?d=Egetl-<x^ zJ4*H=+48ECrvcz>ZH<CbD78768BXKdQO8=5Txz*oaE{R^ZIr+&aNEUY<`<LKZrtqk z3e-n&h)xwGx9wF)$u3opbv8Docx{0Rpn#-cmS=@NWO?;BxaDu|wE{(%MjomCeCV`@ zU{3_&5tKwJvNt<AprO954uHJ5x%ql$edx%xFHUvpXL=gMW&j{tNt5?ZP7TfY_Im}= z+S-~w%}CO1V!4>}6@qrHLSnb9NPd3rKHt>%Y^xBdt*Q)MjgG(A!GxLIe*QKAMF>SG za6HeuJUm>BOFk`XPtlp8ymSDLjRBfgDC610LkAqJ%B*2UB!(3sN+v>>SV&YN0i=N} z_hD|)+H@o_=4D_1=*9q}mP@}%mS*j7v7w?+C2N!VFc8o*oE!&>o8!OCE%+r%Txr!6 z4Tm>-fgYcju#fchbnl}aA!_10?eDY;Wt@Y-!nDTE{60I0vKDHi)!{5F|6w#5O@OZ( z;ILB^rjDYiU%kbASG{apF2o-E55@6Fo$sn#Sb44IpVSAAWoZ#j3<d+*+S+y$syQl+ hJE$vb8ebGX{{y=9=$QvAQ2GD>002ovPDHLkV1msIXo&y- literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/yoursearches_search_last_performed.png b/modules/webstyle/img/yoursearches_search_last_performed.png new file mode 100644 index 0000000000000000000000000000000000000000..b52ed5bb71c6937ab451a571eb6d476aae1e34bd GIT binary patch literal 661 zcmV;G0&4w<P)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog80T1zq`SJh&0uo6?K~y-)b&@?xTVWW6uU3CT7L|gNP^GkF5uEgYxCw%gRa_*U zx|BkT{sTXj3<V)-iJ}%vM;)9BEg>Z4IEgqMPCUtbb!g7PXnVs8FVAy7pZ5cQkpLuT zXJ-OT0lWwyIB+wi+G@4zcDud+NYm7jkr8^m9wP(RCGZM3p<1nm5Q3^ek9NE5;^LyJ zQ4|Hn$H!G&UtbqE%PJjItJR>|>2y>b8ym~pxKz#ZZf<UxVQ+6wfDb-GMl6@h&d<;1 zSpa(b`};;w6c!d11iteG?|~S&0$vY!3Wb8IH#aw`)@ro?oMiqqj$^ZZWj!1n9R*;9 zot>S$!L6;Wp+<q9G@H%bFgiM#lUuEpfE0^GOQn+ie*f{b_WON-&j5!>lDN9MnrDjR z*vZMsp&iub=H`r|DAa1TzbcVSIfSo3Ix{ol?(VLQM#E;aX{A!JSS$*pz`;KQ3x$Gl z9Q*L_;L6I1s!5WVB#EkPYir)#-kKyyCiJ&lf$8aKRZFGP(4ZF=7uM_bAzQgzHceA! zXJ-XI0>q6*!zhaKC1hAyT5@@L*{8>HeSK}G(-8;&SCvX7C!}fW#>R%*+uP4B>~Vp^ z!$X1J0MkHve0&`4@9&=lJ^kO^-Fbd~9tKLheZ0uiiHQkMPftT8dK5*r+ig|5-LCa| vJq+~y0leftcmsR_egO_%1NZ{GdrJNT%MwrGizb5800000NkvXXu0mjf6N4YE literal 0 HcmV?d00001 From be0a5b03a911e3044ca33fb1f005d6de80280c9e Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 26 Jul 2011 14:06:22 +0200 Subject: [PATCH 53/83] WebAlert: youralerts interface improvements * Redesigns the "Your Alerts" display interface. * Adds new functionality to display alerts based on a specific search query. (closes #882) --- modules/webalert/lib/webalert.py | 94 ++-- modules/webalert/lib/webalert_templates.py | 408 +++++++++++++----- modules/webalert/lib/webalert_webinterface.py | 16 +- modules/websearch/lib/websearch_templates.py | 68 +-- modules/webstyle/css/invenio.css | 122 +++++- modules/webstyle/img/youralerts_alert.png | Bin 0 -> 991 bytes .../webstyle/img/youralerts_alert_dates.png | Bin 0 -> 661 bytes .../webstyle/img/youralerts_alert_delete.png | Bin 0 -> 1160 bytes .../webstyle/img/youralerts_alert_edit.png | Bin 0 -> 1213 bytes .../webstyle/img/youralerts_alert_search.png | Bin 0 -> 803 bytes 10 files changed, 531 insertions(+), 177 deletions(-) create mode 100644 modules/webstyle/img/youralerts_alert.png create mode 100644 modules/webstyle/img/youralerts_alert_dates.png create mode 100644 modules/webstyle/img/youralerts_alert_delete.png create mode 100644 modules/webstyle/img/youralerts_alert_edit.png create mode 100644 modules/webstyle/img/youralerts_alert_search.png diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 0898ae0a93..127a76a3c9 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -173,59 +173,88 @@ def perform_add_alert(alert_name, frequency, notification, run_sql(query, params) out = _("The alert %s has been added to your profile.") out %= '<b>' + cgi.escape(alert_name) + '</b>' - out += perform_request_youralerts_display(uid, ln=ln) + out += perform_request_youralerts_display(uid, idq=None, ln=ln) return out def perform_request_youralerts_display(uid, + idq=None, ln=CFG_SITE_LANG): """ - Display a list of the user defined alerts. + Display a list of the user defined alerts. If a specific query id is defined + only the user alerts based on that query appear. + @param uid: The user id @type uid: int + + @param idq: The specified query id for which to display the user alerts + @type idq: int + @param ln: The interface language @type ln: string + @return: HTML formatted list of the user defined alerts. """ # set variables out = "" + if idq: + idq_clause = "AND uqb.id_query=%i" % (idq,) + else: + idq_clause = "" + # query the database - query = """ SELECT q.id, q.urlargs, - a.id_basket, b.name, - a.alert_name, a.frequency,a.notification, - DATE_FORMAT(a.date_creation,'%%Y-%%m-%%d %%H:%%i:%%s'), - DATE_FORMAT(a.date_lastrun,'%%Y-%%m-%%d %%H:%%i:%%s') - FROM user_query_basket a LEFT JOIN query q ON a.id_query=q.id - LEFT JOIN bskBASKET b ON a.id_basket=b.id - WHERE a.id_user=%s - ORDER BY a.alert_name ASC """ - res = run_sql(query, (uid,)) + query = """ SELECT q.id, + q.urlargs, + uqb.id_basket, + bsk.name, + uqb.alert_name, + uqb.frequency, + uqb.notification, + DATE_FORMAT(uqb.date_creation,'%s'), + DATE_FORMAT(uqb.date_lastrun,'%s') + FROM user_query_basket uqb + LEFT JOIN query q + ON uqb.id_query=q.id + LEFT JOIN bskBASKET bsk + ON uqb.id_basket=bsk.id + WHERE uqb.id_user=%%s + %s + ORDER BY uqb.alert_name ASC""" % ('%%Y-%%m-%%d %%H:%%i:%%s', + '%%Y-%%m-%%d %%H:%%i:%%s', + idq_clause,) + params = (uid,) + result = run_sql(query, params) + alerts = [] - for (qry_id, qry_args, - bsk_id, bsk_name, - alrt_name, alrt_frequency, alrt_notification, alrt_creation, alrt_last_run) in res: + for (query_id, + query_args, + bsk_id, + bsk_name, + alert_name, + alert_frequency, + alert_notification, + alert_creation, + alert_last_run) in result: try: - if not qry_id: + if not query_id: raise StandardError("""\ Warning: I have detected a bad alert for user id %d. It seems one of his/her alert queries was deleted from the 'query' table. Please check this and delete it if needed. Otherwise no problem, I'm continuing with the other alerts now. -Here are all the alerts defined by this user: %s""" % (uid, repr(res))) - alerts.append({ - 'queryid' : qry_id, - 'queryargs' : qry_args, - 'textargs' : get_textual_query_info_from_urlargs(qry_args, ln=ln), - 'userid' : uid, - 'basketid' : bsk_id, - 'basketname' : bsk_name, - 'alertname' : alrt_name, - 'frequency' : alrt_frequency, - 'notification' : alrt_notification, - 'created' : convert_datetext_to_dategui(alrt_creation), - 'lastrun' : convert_datetext_to_dategui(alrt_last_run) - }) +Here are all the alerts defined by this user: %s""" % (uid, repr(result))) + alerts.append({'queryid' : query_id, + 'queryargs' : query_args, + 'textargs' : get_textual_query_info_from_urlargs(query_args, ln=ln), + 'userid' : uid, + 'basketid' : bsk_id, + 'basketname' : bsk_name, + 'alertname' : alert_name, + 'frequency' : alert_frequency, + 'notification' : alert_notification, + 'created' : alert_creation, + 'lastrun' : alert_last_run}) except StandardError: register_exception(alert_admin=True) @@ -233,6 +262,7 @@ def perform_request_youralerts_display(uid, out = webalert_templates.tmpl_youralerts_display(ln=ln, alerts=alerts, guest=isGuestUser(uid), + idq=idq, guesttxt=warning_guest_user(type="alerts", ln=ln)) return out @@ -261,7 +291,7 @@ def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG) out += "The alert <b>%s</b> has been removed from your profile.<br /><br />\n" % cgi.escape(alert_name) else: out += "Unable to remove alert <b>%s</b>.<br /><br />\n" % cgi.escape(alert_name) - out += perform_request_youralerts_display(uid, ln=ln) + out += perform_request_youralerts_display(uid, idq=None, ln=ln) return out @@ -321,7 +351,7 @@ def perform_update_alert(alert_name, frequency, notification, id_basket, id_quer run_sql(query, params) out += _("The alert %s has been successfully updated.") % ("<b>" + cgi.escape(alert_name) + "</b>",) - out += "<br /><br />\n" + perform_request_youralerts_display(uid, ln=ln) + out += "<br /><br />\n" + perform_request_youralerts_display(uid, idq=None, ln=ln) return out def is_selected(var, fld): diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 934e7113f6..26844b276e 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -19,19 +19,23 @@ import cgi import time +from urlparse import parse_qs +from datetime import date as datetime_date from invenio.config import \ CFG_WEBALERT_ALERT_ENGINE_EMAIL, \ CFG_SITE_NAME, \ CFG_SITE_SUPPORT_EMAIL, \ CFG_SITE_URL, \ + CFG_SITE_LANG, \ + CFG_SITE_SECURE_URL, \ CFG_WEBALERT_MAX_NUM_OF_RECORDS_IN_ALERT_EMAIL, \ CFG_SITE_RECORD from invenio.messages import gettext_set_language from invenio.htmlparser import get_as_text, wrap, wrap_records from invenio.urlutils import create_html_link - from invenio.search_engine import guess_primary_collection_of_a_record, get_coll_ancestors +from invenio.dateutils import convert_datetext_to_datestruct class Template: def tmpl_errorMsg(self, ln, error_msg, rest = ""): @@ -260,10 +264,13 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio def tmpl_youralerts_display(self, ln, alerts, + idq, guest, guesttxt): """ - Displays the list of alerts + Displays an HTML formatted list of the user alerts. + If the user has specified a query id, only the user alerts based on that + query will appear. @param ln: The language to display the interface in @type ln: string @@ -282,6 +289,9 @@ def tmpl_youralerts_display(self, 'lastrun' *string* - The last running date @type alerts: list of dictionaries + @param idq: The specified query id for which to display the user alerts + @type idq: int + @param guest: Whether the user is a guest or not @type guest: boolean @@ -293,108 +303,153 @@ def tmpl_youralerts_display(self, # load the right message language _ = gettext_set_language(ln) - out = '<p>' + _("Set a new alert from %(x_url1_open)syour searches%(x_url1_close)s, the %(x_url2_open)spopular alerts%(x_url2_close)s, or the input form.") + '</p>' - out %= {'x_url1_open': '<a href="' + CFG_SITE_URL + '/yoursearches/display?ln=' + ln + '">', - 'x_url1_close': '</a>', - 'x_url2_open': '<a href="' + CFG_SITE_URL + '/youralerts/popular?ln=' + ln + '">', - 'x_url2_close': '</a>', - } - if len(alerts): - out += """<table class="alrtTable"> - <tr class="pageboxlefttop" style="text-align: center;"> - <td style="font-weight: bold">%(no)s</td> - <td style="font-weight: bold">%(name)s</td> - <td style="font-weight: bold">%(search_freq)s</td> - <td style="font-weight: bold">%(notification)s</td> - <td style="font-weight: bold">%(result_basket)s</td> - <td style="font-weight: bold">%(date_run)s</td> - <td style="font-weight: bold">%(date_created)s</td> - <td style="font-weight: bold">%(query)s</td> - <td style="font-weight: bold">%(action)s</td></tr>""" % { - 'no' : _("No"), - 'name' : _("Name"), - 'search_freq' : _("Search checking frequency"), - 'notification' : _("Notification by email"), - 'result_basket' : _("Result in basket"), - 'date_run' : _("Date last run"), - 'date_created' : _("Creation date"), - 'query' : _("Query"), - 'action' : _("Action"), - } - i = 0 - for alert in alerts: - i += 1 - if alert['frequency'] == "day": - frequency = _("daily") - else: - if alert['frequency'] == "week": - frequency = _("weekly") - else: - frequency = _("monthly") + # In case the user has not yet defined any alerts display only the + # following message + if not alerts: + if idq: + msg = _('You have not defined any alerts yet based on that search query.') + msg += "<br />" + msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ + {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define one now')), + 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + else: + msg = _('You have not defined any alerts yet.') + msg += '<br />' + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} + out = '<p>' + msg + '</p>' + return out + + # Diplay a message about the number of alerts. + if idq: + msg = _('You have defined %(number_of_alerts)s alerts based on that search query.') % \ + {'number_of_alerts': '<strong>' + str(len(alerts)) + '</strong>'} + msg += '<br />' + msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ + {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define a new one')), + 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + else: + msg = _('You have defined a total of %(number_of_alerts)s alerts.') % \ + {'number_of_alerts': '<strong>' + str(len(alerts)) + '</strong>'} + msg += '<br />' + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} + out = '<p>' + msg + '</p>' + + counter = 0 + youralerts_display_html = "" + for alert in alerts: + counter += 1 + alert_name = alert['alertname'] + alert_query_id = alert['queryid'] + alert_query_args = alert['queryargs'] + # We don't need the text args, we'll use a local function to do a + # better job. + #alert_text_args = alert['textargs'] + # We don't need the user id. The alerts page is a logged in user + # only page anyway. + #alert_user_id = alert['userid'] + alert_basket_id = alert['basketid'] + alert_basket_name = alert['basketname'] + alert_frequency = alert['frequency'] + alert_notification = alert['notification'] + alert_creation_date = alert['created'] + alert_last_run_date = alert['lastrun'] + + alert_details_frequency = _('Runs') + '&nbsp;' + \ + (alert_frequency == 'day' and '<strong>' + _('daily') + '</strong>' or \ + alert_frequency == 'week' and '<strong>' + _('weekly') + '</strong>' or \ + alert_frequency == 'month' and '<strong>' + _('monthly') + '</strong>') + alert_details_notification = alert_notification == 'y' and _('You are notified by <strong>e-mail</strong>') or \ + alert_notification == 'n' and '' + alert_details_basket = alert_basket_name and _('The results are automatically added to your basket:') + \ + '&nbsp;' + '<strong>' + cgi.escape(alert_basket_name) + '</strong>' or '' + alert_details_frequency_notification_basket = alert_details_frequency + \ + (alert_details_notification and \ + '&nbsp;/&nbsp;' + \ + alert_details_notification) + \ + (alert_details_basket and \ + '&nbsp;/&nbsp;' + \ + alert_details_basket) + + alert_details_search_query = get_html_user_friendly_alert_query_args(alert_query_args, ln) + + alert_details_creation_date = get_html_user_friendly_date_from_datetext(alert_creation_date, True, False, ln) + alert_details_last_run_date = get_html_user_friendly_date_from_datetext(alert_last_run_date, True, False, ln) + alert_details_creation_last_run_dates = _('Created:') + '&nbsp;' + \ + alert_details_creation_date + \ + '&nbsp;/&nbsp;' + \ + _('Last run:') + '&nbsp;' + \ + alert_details_last_run_date + + alert_details_options_edit = create_html_link('%s/youralerts/modify' % \ + (CFG_SITE_SECURE_URL,), + {'ln' : ln, + 'idq' : alert_query_id, + 'name' : alert_name, + 'freq' : alert_frequency, + 'notif' : alert_notification, + 'idb' : alert_basket_id, + 'old_idb': alert_basket_id}, + _('Edit')) + alert_details_options_delete = create_html_link('%s/youralerts/remove' % \ + (CFG_SITE_SECURE_URL,), + {'ln' : ln, + 'idq' : alert_query_id, + 'name' : alert_name, + 'idb' : alert_basket_id}, + _('Delete')) + alert_details_options = '<img src="%s/img/youralerts_alert_edit.png" />' % (CFG_SITE_URL,) + \ + alert_details_options_edit + \ + '&nbsp;&nbsp;&nbsp;' + \ + '<img src="%s/img/youralerts_alert_delete.png" />' % (CFG_SITE_URL,) + \ + alert_details_options_delete + + youralerts_display_html += """ + <tr> + <td class="youralerts_display_table_counter"> + %(counter)i. + </td> + <td class="youralerts_display_table_content" onMouseOver='this.className="youralerts_display_table_content_mouseover"' onMouseOut='this.className="youralerts_display_table_content"'> + <div class="youralerts_display_table_content_container_left"> + <div class="youralerts_display_table_content_name">%(alert_name)s</div> + <div class="youralerts_display_table_content_details">%(alert_details_frequency_notification_basket)s</div> + <div class="youralerts_display_table_content_search_query">%(alert_details_search_query)s</div> + </div> + <div class="youralerts_display_table_content_container_right"> + <div class="youralerts_display_table_content_options">%(alert_details_options)s</div> + </div> + <div class="youralerts_display_table_content_clear"></div> + <div class="youralerts_display_table_content_dates">%(alert_details_creation_last_run_dates)s</div> + </td> + </tr>""" % {'counter': counter, + 'alert_name': cgi.escape(alert_name), + 'alert_details_frequency_notification_basket': alert_details_frequency_notification_basket, + 'alert_details_search_query': alert_details_search_query, + 'alert_details_options': alert_details_options, + 'alert_details_creation_last_run_dates': alert_details_creation_last_run_dates} + + out += """ +<table class="youralerts_display_table" cellspacing="2px"> + <thead class="youralerts_display_table_header"> + <tr> + <td colspan="2"></td> + </tr> + </thead> + <tfoot class="youralerts_display_table_footer"> + <tr> + <td colspan="2"></td> + </tr> + </tfoot> + <tbody> + %(youralerts_display_html)s + </tbody> +</table>""" % {'youralerts_display_html': youralerts_display_html} - if alert['notification'] == "y": - notification = _("yes") - else: - notification = _("no") - - # we clean up the HH:MM part of lastrun, since it is always 00:00 - lastrun = alert['lastrun'].split(',')[0] - created = alert['created'].split(',')[0] - - out += """<tr> - <td style="font-style: italic">#%(index)d</td> - <td style="font-weight: bold; text-wrap:none;">%(alertname)s</td> - <td>%(frequency)s</td> - <td style="text-align:center">%(notification)s</td> - <td style="text-wrap:none;">%(basketname)s</td> - <td style="text-wrap:none;">%(lastrun)s</td> - <td style="text-wrap:none;">%(created)s</td> - <td>%(textargs)s</td> - <td> - %(remove_link)s<br /> - %(modify_link)s<br /> - <a href="%(siteurl)s/search?%(queryargs)s&amp;ln=%(ln)s" style="white-space:nowrap">%(search)s</a> - </td> - </tr>""" % { - 'index' : i, - 'alertname' : cgi.escape(alert['alertname']), - 'frequency' : frequency, - 'notification' : notification, - 'basketname' : alert['basketname'] and cgi.escape(alert['basketname']) \ - or "- " + _("no basket") + " -", - 'lastrun' : lastrun, - 'created' : created, - 'textargs' : alert['textargs'], - 'queryid' : alert['queryid'], - 'basketid' : alert['basketid'], - 'freq' : alert['frequency'], - 'notif' : alert['notification'], - 'ln' : ln, - 'modify_link': create_html_link("./modify", - {'ln': ln, - 'idq': alert['queryid'], - 'name': alert['alertname'], - 'freq': frequency, - 'notif':notification, - 'idb':alert['basketid'], - 'old_idb':alert['basketid']}, - _("Modify")), - 'remove_link': create_html_link("./remove", - {'ln': ln, - 'idq': alert['queryid'], - 'name': alert['alertname'], - 'idb':alert['basketid']}, - _("Remove")), - 'siteurl' : CFG_SITE_URL, - 'search' : _("Execute search"), - 'queryargs' : cgi.escape(alert['queryargs']) - } - - out += '</table>' - - out += '<p>' + (_("You have defined %s alerts.") % ('<b>' + str(len(alerts)) + '</b>' )) + '</p>' - if guest: - out += guesttxt return out def tmpl_alert_email_title(self, name): @@ -627,3 +682,150 @@ def tmpl_youralerts_popular(self, out += "</table><br />\n" return out + +def get_html_user_friendly_alert_query_args(args, + ln=CFG_SITE_LANG): + """ + Internal function. + Returns an HTML formatted user friendly description of a search query's + arguments. + + @param args: The search query arguments as they apear in the search URL + @type args: string + + @param ln: The language to display the interface in + @type ln: string + + @return: HTML formatted user friendly description of a search query's + arguments + """ + + # Load the right language + _ = gettext_set_language(ln) + + # Arguments dictionary + dict = parse_qs(args) + + if not dict.has_key('p') and not dict.has_key('p1') and not dict.has_key('p2') and not dict.has_key('p3'): + search_patterns_html = _('Searching for everything') + else: + search_patterns_html = _('Searching for') + ' ' + if dict.has_key('p'): + search_patterns_html += '<strong>' + cgi.escape(dict['p'][0]) + '</strong>' + if dict.has_key('f'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f'][0]) + '</strong>' + if dict.has_key('p1'): + if dict.has_key('p'): + search_patterns_html += ' ' + _('and') + ' ' + search_patterns_html += '<strong>' + cgi.escape(dict['p1'][0]) + '</strong>' + if dict.has_key('f1'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f1'][0]) + '</strong>' + if dict.has_key('p2'): + if dict.has_key('p') or dict.has_key('p1'): + if dict.has_key('op1'): + search_patterns_html += ' %s ' % (dict['op1'][0] == 'a' and _('and') or \ + dict['op1'][0] == 'o' and _('or') or \ + dict['op1'][0] == 'n' and _('and not') or + ', ',) + search_patterns_html += '<strong>' + cgi.escape(dict['p2'][0]) + '</strong>' + if dict.has_key('f2'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f2'][0]) + '</strong>' + if dict.has_key('p3'): + if dict.has_key('p') or dict.has_key('p1') or dict.has_key('p2'): + if dict.has_key('op2'): + search_patterns_html += ' %s ' % (dict['op2'][0] == 'a' and _('and') or \ + dict['op2'][0] == 'o' and _('or') or \ + dict['op2'][0] == 'n' and _('and not') or + ', ',) + search_patterns_html += '<strong>' + cgi.escape(dict['p3'][0]) + '</strong>' + if dict.has_key('f3'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f3'][0]) + '</strong>' + + if not dict.has_key('c') and not dict.has_key('cc'): + collections_html = _('in all the collections') + else: + collections_html = _('in the following collection(s)') + ': ' + if dict.has_key('c'): + collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in dict['c']) + elif dict.has_key('cc'): + collections_html += '<strong>' + cgi.escape(dict['cc'][0]) + '</strong>' + + search_query_args_html = search_patterns_html + '<br />' + collections_html + + return search_query_args_html + + +def get_html_user_friendly_date_from_datetext(given_date, + show_full_date=True, + show_full_time=True, + ln=CFG_SITE_LANG): + """ + Internal function. + Returns an HTML formatted user friendly description of a search query's + last run date. + + @param given_date: The search query last run date in the following format: + '2005-11-16 15:11:57' + @type given_date: string + + @param show_full_date: show the full date as well + @type show_full_date: boolean + + @param show_full_time: show the full time as well + @type show_full_time: boolean + + @param ln: The language to display the interface in + @type ln: string + + @return: HTML formatted user friendly description of a search query's + last run date + """ + + # Load the right language + _ = gettext_set_language(ln) + + # Calculate how many days old the search query is base on the given date + # and today + # given_date_datestruct[0] --> year + # given_date_datestruct[1] --> month + # given_date_datestruct[2] --> day in month + given_date_datestruct = convert_datetext_to_datestruct(given_date) + today = datetime_date.today() + if given_date_datestruct[0] != 0 and \ + given_date_datestruct[1] != 0 and \ + given_date_datestruct[2] != 0: + days_old = (today - datetime_date(given_date_datestruct.tm_year, + given_date_datestruct.tm_mon, + given_date_datestruct.tm_mday)).days + if days_old == 0: + out = _('Today') + elif days_old < 7: + out = str(days_old) + ' ' + _('day(s) ago') + elif days_old == 7: + out = _('A week ago') + elif days_old < 14: + out = _('More than a week ago') + elif days_old == 14: + out = _('Two weeks ago') + elif days_old < 30: + out = _('More than two weeks ago') + elif days_old == 30: + out = _('A month ago') + elif days_old < 180: + out = _('More than a month ago') + elif days_old < 365: + out = _('More than six months ago') + else: + out = _('More than a year ago') + if show_full_date: + out += '<span style="color: gray;">' + \ + '&nbsp;' + _('on') + '&nbsp;' + \ + given_date.split()[0] + '</span>' + if show_full_time: + out += '<span style="color: gray;">' + \ + '&nbsp;' + _('at') + '&nbsp;' + \ + given_date.split()[1] + '</span>' + else: + out = _('Unknown') + + return out diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index a7afc94dbf..a189e9c7fd 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -239,7 +239,8 @@ def modify(self, req, form): def display(self, req, form): - argd = wash_urlargd(form, {}) + argd = wash_urlargd(form, {'idq': (int, None), + }) uid = getUid(req) @@ -274,12 +275,13 @@ def display(self, req, form): register_exception(suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") return page(title=_("Your Alerts"), - body=perform_request_youralerts_display(uid, ln = argd['ln']), - navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { - 'sitesecureurl' : CFG_SITE_SECURE_URL, - 'ln': argd['ln'], - 'account' : _("Your Account"), - }, + body=perform_request_youralerts_display(uid, + idq=argd['idq'], + ln=argd['ln']), + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % \ + {'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account")}, description=_("%s Personalize, Display alerts") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), uid=uid, diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 3bcc9f1771..3d9fa0a9c7 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -29,6 +29,7 @@ from urllib import quote, urlencode from urlparse import parse_qs from xml.sax.saxutils import escape as xml_escape +from datetime import date as datetime_date from invenio.config import \ CFG_WEBSEARCH_LIGHTSEARCH_PATTERN_BOX_WIDTH, \ @@ -82,9 +83,7 @@ from invenio.webinterface_handler import wash_urlargd from invenio.bibrank_citation_searcher import get_cited_by_count from invenio.webuser import session_param_get - from invenio.intbitset import intbitset - from invenio.websearch_external_collections import external_collection_get_state, get_external_collection_engine from invenio.websearch_external_collections_utils import get_collection_id from invenio.websearch_external_collections_config import CFG_EXTERNAL_COLLECTION_MAXRESULTS @@ -95,9 +94,6 @@ from invenio import hepdatadisplayutils from invenio.dateutils import convert_datetext_to_datestruct -from datetime import date as datetime_date - -#from invenio.websearch_yoursearches import count_user_alerts_for_given_query _RE_PUNCTUATION = re.compile(CFG_BIBINDEX_CHARS_PUNCTUATION) _RE_SPACES = re.compile(r"\s+") @@ -5145,26 +5141,27 @@ def tmpl_yoursearches_display(self, # following message if not search_queries: if p: - out = _("You have not searched for anything yet including the terms %(p)s. You may perform that %(x_url_open)ssearch%(x_url_close)s for the first time now.") % \ + msg = _("You have not searched for anything yet including the terms %(p)s. You may perform that %(x_url_open)ssearch%(x_url_close)s for the first time now.") % \ {'p': '<strong>' + cgi.escape(p) + '</strong>', 'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/search?ln=' + ln + '&amp;p=' + cgi.escape(p) + '">', 'x_url_close': '</a>'} else: - out = _("You have not searched for anything yet. You may want to start by the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ + msg = _("You have not searched for anything yet. You may want to start by the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ {'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/?ln=' + ln +'">', 'x_url_close': '</a>'} + out = '<p>' + msg + '</p>' return out # Diplay a message about the number of searches. if p: msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches including the term %(p)s.") % \ - {'searches_distinct': nb_queries_distinct, - 'searches_total': nb_queries_total, + {'searches_distinct': '<strong>' + str(nb_queries_distinct) + '</strong>', + 'searches_total': '<strong>' + str(nb_queries_total) + '</strong>', 'p': '<strong>' + cgi.escape(p) + '</strong>'} else: msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches.") % \ - {'searches_distinct': nb_queries_distinct, - 'searches_total': nb_queries_total} + {'searches_distinct': '<strong>' + str(nb_queries_distinct) + '</strong>', + 'searches_total': '<strong>' + str(nb_queries_total) + '</strong>'} out = '<p>' + msg + '</p>' # Search form @@ -5191,7 +5188,7 @@ def tmpl_yoursearches_display(self, search_query_details = get_html_user_friendly_search_query_args(search_query_args, ln) - search_query_last_performed = get_html_user_friendly_search_query_lastrun(search_query_lastrun, ln) + search_query_last_performed = get_html_user_friendly_date_from_datetext(search_query_lastrun, ln) search_query_options_search = """<a href="%s/search?%s"><img src="%s/img/yoursearches_search.png" />%s</a>""" % \ (CFG_SITE_SECURE_URL, cgi.escape(search_query_args), CFG_SITE_URL, _('Search again')) @@ -5337,16 +5334,24 @@ def get_html_user_friendly_search_query_args(args, return search_query_args_html -def get_html_user_friendly_search_query_lastrun(lastrun, - ln=CFG_SITE_LANG): +def get_html_user_friendly_date_from_datetext(given_date, + show_full_date=True, + show_full_time=True, + ln=CFG_SITE_LANG): """ Internal function. Returns an HTML formatted user friendly description of a search query's last run date. - @param lastrun: The search query last run date in the following format: + @param given_date: The search query last run date in the following format: '2005-11-16 15:11:57' - @type lastrun: string + @type given_date: string + + @param show_full_date: show the full date as well + @type show_full_date: boolean + + @param show_full_time: show the full time as well + @type show_full_time: boolean @param ln: The language to display the interface in @type ln: string @@ -5358,16 +5363,19 @@ def get_html_user_friendly_search_query_lastrun(lastrun, # Load the right language _ = gettext_set_language(ln) - # Calculate how many days old the search query is base on the lastrun date + # Calculate how many days old the search query is base on the given date # and today - lastrun_datestruct = convert_datetext_to_datestruct(lastrun) + # given_date_datestruct[0] --> year + # given_date_datestruct[1] --> month + # given_date_datestruct[2] --> day in month + given_date_datestruct = convert_datetext_to_datestruct(given_date) today = datetime_date.today() - if lastrun_datestruct.tm_year != 0 and \ - lastrun_datestruct.tm_mon != 0 and \ - lastrun_datestruct.tm_mday != 0: - days_old = (today - datetime_date(lastrun_datestruct.tm_year, - lastrun_datestruct.tm_mon, - lastrun_datestruct.tm_mday)).days + if given_date_datestruct[0] != 0 and \ + given_date_datestruct[1] != 0 and \ + given_date_datestruct[2] != 0: + days_old = (today - datetime_date(given_date_datestruct.tm_year, + given_date_datestruct.tm_mon, + given_date_datestruct.tm_mday)).days if days_old == 0: out = _('Today') elif days_old < 7: @@ -5388,10 +5396,14 @@ def get_html_user_friendly_search_query_lastrun(lastrun, out = _('More than six months ago') else: out = _('More than a year ago') - out += '<span style="color: gray;">' + \ - '&nbsp;' + _('on') + '&nbsp;' + lastrun.split()[0] + \ - '&nbsp;' + _('at') + '&nbsp;' + lastrun.split()[1] + \ - '</span>' + if show_full_date: + out += '<span style="color: gray;">' + \ + '&nbsp;' + _('on') + '&nbsp;' + \ + given_date.split()[0] + '</span>' + if show_full_time: + out += '<span style="color: gray;">' + \ + '&nbsp;' + _('at') + '&nbsp;' + \ + given_date.split()[1] + '</span>' else: out = _('Unknown') diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index b8753925a5..f03c01ca69 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2580,6 +2580,120 @@ a:hover.bibMergeImgClickable img { border: 1px solid black; padding: 3px; } + +.youralerts_display_table { + border: 1px solid #bbbbbb; +} +.youralerts_display_table_header td { + background-color: #7a9bdd; + padding: 2px; + border-bottom: 1px solid #3366cc; + border-right: 1px solid #3366cc; +} +.youralerts_display_table_footer td { + background-color: #7a9bdd; + color: white; + padding: 2px; + font-size: 80%; + text-align: center; + border-bottom: 1px solid #3366cc; + border-right: 1px solid #3366cc; + white-space: nowrap; +} +.youralerts_display_table_footer img { + border: none; + margin: 0px 3px; + vertical-align: bottom; +} +.youralerts_display_footer a, .youralerts_display_footer a:link, .youralerts_display_footer a:visited, .youralerts_display_footer a:active { + text-decoration: none; + color: #333; +} +.youralerts_display_footer a:hover { + text-decoration: underline; + color: #000; +} +.youralerts_display_footer img { + border: none; + vertical-align: bottom; +} +.youralerts_display_table_content { + background-color: #f2f2fa; + padding: 5px; + text-align: left; + vertical-align: top; +} +.youralerts_display_table_content_mouseover { + background-color: #ebebfa; + padding: 5px; + text-align: left; + vertical-align: top; +} +.youralerts_display_table_content_container_left { + float: left; + width: 75%; +} +.youralerts_display_table_content_container_right { + float: right; + width: 25%; +} +.youralerts_display_table_content_clear { + clear: both; +} +.youralerts_display_table_counter { + background-color: #f2f2fa; + padding: 5px 2px; + text-align: right; + vertical-align: top; + font-size: 80%; + color: gray; +} +.youralerts_display_table_content_name { + margin-bottom: 5px; + font-weight: bold; + position: relative; + left: 0px; + top: 0px; +} +.youralerts_display_table_content_details { + margin-bottom: 5px; + font-size: 80%; + position: relative; + left: 0px; +} +.youralerts_display_table_content_search_query { + margin-bottom: 5px; + font-size: 80%; + position: relative; + left: 0px; +} +.youralerts_display_table_content_dates { + position: relative; + font-size: 70%; + text-align: right; + white-space: nowrap; + color: gray; +} +.youralerts_display_table_content_options { + position: relative; + top: 0px; + right: 0px; + margin-bottom: 5px; + font-size: 80%; + text-align: right; +} +.youralerts_display_table_content_options img{ + vertical-align: top; + margin-right: 3px; +} +.youralerts_display_table_content_options a, .youralerts_display_table_content_options a:link, .youralerts_display_table_content_options a:visited, .youralerts_display_table_content_options a:active { + text-decoration: underline; + color: #333; +} +.youralerts_display_table_content_options a:hover { + text-decoration: underline; + color: #000; +} /* end of WebAlert module */ /* WebSearch module - YourSearches */ @@ -2587,29 +2701,23 @@ a:hover.bibMergeImgClickable img { border: 1px solid #bbbbbb; } .websearch_yoursearches_table_header td { -/* background-color: #63a5cd;*/ background-color: #7a9bdd; color: white; padding: 2px; font-weight: bold; font-size: 75%; text-transform: uppercase; -/* border-bottom: 1px solid #327aa5; - border-right: 1px solid #327aa5;*/ border-bottom: 1px solid #3366cc; border-right: 1px solid #3366cc; white-space: nowrap; vertical-align: top; } .websearch_yoursearches_table_footer td { -/* background-color: #63a5cd;*/ background-color: #7a9bdd; color: white; padding: 5px; font-size: 80%; text-align: center; -/* border-bottom: 1px solid #327aa5; - border-right: 1px solid #327aa5;*/ border-bottom: 1px solid #3366cc; border-right: 1px solid #3366cc; white-space: nowrap; @@ -2656,7 +2764,7 @@ a:hover.bibMergeImgClickable img { font-size: 80%; } .websearch_yoursearches_table_content_options img{ - vertical-align: middle; + vertical-align: top; margin-right: 3px; } .websearch_yoursearches_table_content_options a, .websearch_yoursearches_table_content_options a:link, .websearch_yoursearches_table_content_options a:visited, .websearch_yoursearches_table_content_options a:active { diff --git a/modules/webstyle/img/youralerts_alert.png b/modules/webstyle/img/youralerts_alert.png new file mode 100644 index 0000000000000000000000000000000000000000..c036ff6de4ff10cfb3b89b0c52740898b78639ee GIT binary patch literal 991 zcmV<510ei~P)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog5HYCq_9^U`}16)Z&K~y-)os?flWOWqBKlhF^G-{&`W4dmuumeR;z8Ds=QZGry zR^ZYSNwg1vU=*=0u}B0N^blDfXh|uqe|B^R1+f>w5Rx83Uv#ml(IHCMomsEWAF^>~ z?zaaWadt<DzHs6G?z!iC&iQ^1KVx`!c-iaqUI8Ew2xKKme_WL@Gc%I~*x%pZB$-S~ zlC+6^`dcItQ8*lyB#n-arphvz%>7U(R1e^AIE<%`o;KfUq0*R`n8><ZE($n0I-<L~ z+leHttgJ|qR##W`;muD3fNk3<;QE_4q)i08Z3KhCT!E&g>YAge7Iv+Z=3}v#78e&) z3JVJhipS#`85v0xPb=y0@KCv2PN7gph0@l>D^m1D{r&?wO|UGhB>NL}m!(ZkABV|~ z!TIR3M)<VG4LE@#*w275G&Gd+`FxU8GRrhgVzHQMnx?a-r^iN;W@l$*S(eU9CP|iM zDHse&k_HC{YXHoAK2KLy7v<&UOioUoGEYrSQCV5Zy;zFdlOf!0_i-SX%Sn=K+g3at zS0obA&d!eZ_V)Dp%g;#JHR)I6t>UYqgk&=LNK1E@o|iU`75cvaSXLvS&nutL<8(S7 zop{@Te@@;X2crq2Y<_;;E+x_I>>bCQJ~*)pCBa+8m*@KMc`v&8ER>l*6Xc@M{}Wkb zdU`tB(9lp84u|RJ=%BH&5!<#o&zFbca5&iB-e!4unU<Co*4Nk5nM|g!x3_m6pv*K) z+S=M^Z*NBu(P)&Vr6ro1n`v!rEh0Nxbai!=t*tFOJ3I0F{rLTU0M^#likk}r0@;ES z1r*#!r_&lAA6F<8QZkv;+}xbT#>SLLBqT|@ySutjSE10?WlH|y)>uEPsjf)B`r5Vn zk_8IHnM_8pSgh#V!^OvMPwBG{Ov(OR+VY;RgS~;SzQE}S=2;0+sT2nX2e@1=d_Erl zhEeQm;rz|FWAwZsxLXc+3yeY>hCc=%1Yk-s5{U%aY!;8lLv?j^N#e7(cmDx(?w|2q z5T#b3;S#+67B2ypjDdlH3ez;}|I5aBJf8eVxB74OUEhB9y(^$YFtR`%coL`wemlM@ zC`VuXh+**m@Z9A_Zulzbto3m5VlBpFE;!7<ZXCr^{PiHsV)ReG`xmaLi`nT|P!9kA N002ovPDHLkV1j_J%S-?O literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youralerts_alert_dates.png b/modules/webstyle/img/youralerts_alert_dates.png new file mode 100644 index 0000000000000000000000000000000000000000..b52ed5bb71c6937ab451a571eb6d476aae1e34bd GIT binary patch literal 661 zcmV;G0&4w<P)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog80T1zq`SJh&0uo6?K~y-)b&@?xTVWW6uU3CT7L|gNP^GkF5uEgYxCw%gRa_*U zx|BkT{sTXj3<V)-iJ}%vM;)9BEg>Z4IEgqMPCUtbb!g7PXnVs8FVAy7pZ5cQkpLuT zXJ-OT0lWwyIB+wi+G@4zcDud+NYm7jkr8^m9wP(RCGZM3p<1nm5Q3^ek9NE5;^LyJ zQ4|Hn$H!G&UtbqE%PJjItJR>|>2y>b8ym~pxKz#ZZf<UxVQ+6wfDb-GMl6@h&d<;1 zSpa(b`};;w6c!d11iteG?|~S&0$vY!3Wb8IH#aw`)@ro?oMiqqj$^ZZWj!1n9R*;9 zot>S$!L6;Wp+<q9G@H%bFgiM#lUuEpfE0^GOQn+ie*f{b_WON-&j5!>lDN9MnrDjR z*vZMsp&iub=H`r|DAa1TzbcVSIfSo3Ix{ol?(VLQM#E;aX{A!JSS$*pz`;KQ3x$Gl z9Q*L_;L6I1s!5WVB#EkPYir)#-kKyyCiJ&lf$8aKRZFGP(4ZF=7uM_bAzQgzHceA! zXJ-XI0>q6*!zhaKC1hAyT5@@L*{8>HeSK}G(-8;&SCvX7C!}fW#>R%*+uP4B>~Vp^ z!$X1J0MkHve0&`4@9&=lJ^kO^-Fbd~9tKLheZ0uiiHQkMPftT8dK5*r+ig|5-LCa| vJq+~y0leftcmsR_egO_%1NZ{GdrJNT%MwrGizb5800000NkvXXu0mjf6N4YE literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youralerts_alert_delete.png b/modules/webstyle/img/youralerts_alert_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..6d28c094b7e9144e2de856ffb0731630b1516b29 GIT binary patch literal 1160 zcmV;31b6$1P)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+XopC76n_#U||3N1O-V%K~y-)m6Kga+gBLIpY#8S=EE3mlBsoB#cmy5OxxKBlL=#3 zr-f3ctm|;lLN5gG27|$1W0$+=ys;>ib)Zs-nt&78&?*xZ>_BmAXA`T0I<1;4iGO2a z^8YyRE;{X86}I2qIlS+4&U>Ecu+GlTiTwQhcK`rhuU8`?+PratSbu-N1_1GR{2_LB zc8G{*<N9?vC|h1$reH8gnZyp=Zg0;>nM`KO=kpZ+09Vt3c<mN@m&n9=dwVsL$%K7W zRYh}ivkW4l$;n9~A{x5(331Npe)aip8RwiBV*r4u*Ivczb?+lMI!cn??>F{kDja!Y zWOkMcY&Hh3*Q<rYVHz13p`#cc9;QenLS0>52>_4)z~yqGp`iikbQ-cOV`yjyZ>)x3 zom;@$xw(+eRp5iUd0YLJD;!?0SBu4BN2+J`QnY&cGOaZ=(dwm31VF%Xyakv8Wo2~F z?NR~Yp4+7d%F2j!bad!Wr;~{2$VNdB5Dte0K@jAYmKF{oqJe<{ip65YIX`rzSS&_^ zgM&mw)ZX5n2LRA@ojB)|PN%7-r{|!!r@NbipM83;UTC;54ggHobrMApM1*KG3Pn*M z$};4Z7RVkCQYI0k>oBVZj7a!-Ln4uY&1M5*3`B&D(H}uQcOl#DAR^H2E{%^2)-uB| z$S@4Zvi!op59;1SUQQlBgvi#qa&G3ngZX?u<HaXpIDPXQ(W(=KtSlIY0caXjM(gZ2 zAAUJqvY8`izcjo4_=3%1&MtGD{6SGuk62${pH@^<Bn5*()Ya9&<MDuVj^lj!H=-zF zWn~2u6BDScti=5M{B9<b@wB$K#sNSQ1Oe66)u^ed0TCe-3Sn$)4CUqJsH&<uB=*lk zXJ%#)2n0}HUyst#Qk0gK0s!Xb=D^Kn0)X$`W-zbUt2H(@N}?zp5@L)YnM}g(_rq$n zqPDgc<KyE{5(%9BBZR`=7Qrl5Y^Jtx=1w<cixp~e7r!@OMX~(^czO>@D~n$qI$&Q| zRaGd80+-8mu(!{Xfj+E#@fBXFsDPB43(;(bBuR*Et|QM~2whcSq*5@FNrZk~*gN!l z5{U%1x3^(3nc#FfpZU!W`|4@>2-QnV;2(d8jimszXcXya6jqZNnRpypJdUT27E_bc zKV7YPyljz(h$)JKoSYok?e^zEVn+vtF^1&RHKe|~19z4MS|kF9;<JEr4zBCuw&z-M z&YZpJlO9g6wzf8_AP5EjB{8}qPe1kb=eOQ;6kgm*?-65+iIPN5{@hCMlGb8_+;uYF zDKjEQoRgfD&G3Kd_<O(}XLkNn$;*LhPjT$-33CMiOqUb`0Q?2H=chcy%Fm~Y9^7^a aj=ump<y}fu&unJ^0000<MNUMnLSTX)<_~xP literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youralerts_alert_edit.png b/modules/webstyle/img/youralerts_alert_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..9af0b3632d0233a719efa1871529f32ccee27e5e GIT binary patch literal 1213 zcmV;u1Va0XP)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xop73<hGKhmimP1UgAXK~y-)jgx;&)O8rgU!U)N?~q>#=V1(qz|5JoNt2=^DCe9T zbFQSJHC^RGnfPB@vp@Qy%|Bdi*jk1g%jJ;li`}`{a8vXgGo7f1fWHQGf_kE!-W>-V z-mmXH{RLAa*Xz&E_PjsO`~5tR+P{Cl+itg40$><MLPTU}@VcnBwzdR7C=|M?#>dCS zX0y@S`rD*33?ornTdRA$UQE-(W=o^<<f)_{i^U!|oz8p!mnUj@Fcvc#>sM&%(4j*K zo6W{F@pzogn>Sk#5$Wpc5)qN^^W7pMa<>1bkV>Uwn!%v~IdS@&0O&54%bb=OdR(Zz zl0KTccI}#A7)HYH_e)1dhdhhVcSqXW|CEOhA4+p`^ThfOKDX)sd3kwMS635_MzLD0 zw70iYT3X6vWP;MtH*h!{*z=3C)@`uw!Z3_PFc_5CXgl5~pYAy+SFZMp&*u}KL)wSm zbjZ4|jtY<uWB>*3Q>P|XQ&UsY;c$q^>;jsm;rIJBP1CIP_4O%4M2;LeBEeu#o{Y>i zO&J*(7N^rGA|g#c@5|Y+ahru?GAXH4N}|!Iw6wI$6tuRs%7u&F(to8-L`04rKRed$ zz9=f0Op3)~K|}}y0))e1B2y7GO(P>cgHR|)L18iOzb<$N2Zt+W3MM8d$jZt>DFwj2 z`=cZiNutpt1KwNQxq02bqhV)AnWibGX=1fn|7CdVcc&QjjdEbuYR+Fcw{K)*c&BnY zo#wL<MU$f4)0QaLu`cRYJIq8pLdD8=EuNl>J7!I??Bm9RmD_$1d+C<XfGlAC_WCcj z9&GuwTOB@pI8jhgpbrcTuy*ZQii(OzrBeLYT%N^ZvEcQ3al74=m6g%c(=!>1#flml z8bScNrfF1ER8Un_g@`abJWOY2C(D;Fr@XxUDY5BKP1EELXB$<O@8i4cqp)Bhb#-+B zcsw2gp-9#N<JgdyNM6#P6i`Z0Qc^-)T^&j(B9RC#my68IOxCPfgZs1_zyCfT)zq?S z<0tg?^-xk=!lS?=3Rnn0O6i%U@4R<AWtxoo?sh7}FcLL2HTv{!D5Z$U<AlRu^78U# z-T-2;7=L?*cws?47TrQ3o**0!lbxMSPw!=HbL@n|V^cksE*I_D{dE99d(y$g!~|nw zW7upq91h2ftC@>ROJmuqui<bw$j!;+>g@<Wxq4|o-HR<fm!*pfF{445qerFjhwlN< zMMO<^F?a6Vr{bCor4+xP=;zFh5Yfkz4BsBd)aS8&)k_2d0m|QcJ7dL)Hv_$Wo<`tH zwRi8{Oik1BpSLMWDawk}<ZgHN$?cm~z3#bjht4yXB{SQ8Yf<`x>bI9<#HON2blobE zsc`Yim6gZF!{ccdT~qu|EWrG<oT8=WTYl_X)zBi#)_>a!ybQbu<N)cwJRlEPuz2yJ b5+L^<z1V3TXNh(`00000NkvXXu0mjfOI}3r literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youralerts_alert_search.png b/modules/webstyle/img/youralerts_alert_search.png new file mode 100644 index 0000000000000000000000000000000000000000..742d87d0eea3ba912475ce8cca9909ff3ccdf13f GIT binary patch literal 803 zcmV+;1Kj+HP)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog5H!>o%n$G|L0-#AmK~y-)osVlsQ*juFfB$oiZo0YcY|5tQWois;n1Q57T4GX$ z%m}>X1#Id>G?bM5qS2Qikj$VP5>i<NMi;Zv%n$^fnMCQxT86g_5yFMLxUI9(hghhn zV9&>w=Y4oz@H`JM<ihO-1JLCaXaN8n^Hx0oAS3%koc7^6L%^M3JvL%}Q~tM*m7A~i z4ry8YV7VtgHM}}X8ayyGY)0d)`;YI=TT1~j>-MWM0umzSh$R4U{7OgrQ=&40fK-bt zSy%Nrx_`~IV_9*Y%tB;DDM>;I3$sx1sX!@+92_=&UY9iJPvumU>2m?d=Egetl-<x^ zJ4*H=+48ECrvcz>ZH<CbD78768BXKdQO8=5Txz*oaE{R^ZIr+&aNEUY<`<LKZrtqk z3e-n&h)xwGx9wF)$u3opbv8Docx{0Rpn#-cmS=@NWO?;BxaDu|wE{(%MjomCeCV`@ zU{3_&5tKwJvNt<AprO954uHJ5x%ql$edx%xFHUvpXL=gMW&j{tNt5?ZP7TfY_Im}= z+S-~w%}CO1V!4>}6@qrHLSnb9NPd3rKHt>%Y^xBdt*Q)MjgG(A!GxLIe*QKAMF>SG za6HeuJUm>BOFk`XPtlp8ymSDLjRBfgDC610LkAqJ%B*2UB!(3sN+v>>SV&YN0i=N} z_hD|)+H@o_=4D_1=*9q}mP@}%mS*j7v7w?+C2N!VFc8o*oE!&>o8!OCE%+r%Txr!6 z4Tm>-fgYcju#fchbnl}aA!_10?eDY;Wt@Y-!nDTE{60I0vKDHi)!{5F|6w#5O@OZ( z;ILB^rjDYiU%kbASG{apF2o-E55@6Fo$sn#Sb44IpVSAAWoZ#j3<d+*+S+y$syQl+ hJE$vb8ebGX{{y=9=$QvAQ2GD>002ovPDHLkV1msIXo&y- literal 0 HcmV?d00001 From 0b260cab297bbc8169903fa01cc547cde40d4ab3 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 23 Aug 2011 16:46:35 +0200 Subject: [PATCH 54/83] WebSearch: fix yoursearches string format bug * Fixes a small bug that caused special characters that had been replaced by the urllib.quote() function to crash during the string formatting. (closes #883) --- modules/websearch/lib/websearch_yoursearches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index 1da6fbfac6..ab9a381277 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -65,7 +65,7 @@ def perform_request_yoursearches_display(uid, if p: p_stripped = p.strip() p_stripped_args = p.split() - sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg) + '%%\'' for p_stripped_arg in p_stripped_args] + sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg).replace('%','%%') + '%%\'' for p_stripped_arg in p_stripped_args] for sql_p_stripped_arg in sql_p_stripped_args: search_clause += """ AND q.urlargs LIKE %s""" % (sql_p_stripped_arg,) From de937a7f6debbc532589e16cf2b25856d2f9cbf2 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 30 Aug 2011 14:34:37 +0200 Subject: [PATCH 55/83] WebSearch & WebAlert: CSS fallback to default * Changes some CSS attributes to match yoursearches and youralerts style to the default Invenio style. * Changes the order of the Personalize menu to be in alphabetical order again after the addition of Your Searches. * Deletes some previously added unneeded images. (addresses #884) --- modules/webalert/lib/webalert_templates.py | 2 +- modules/websearch/lib/websearch_templates.py | 2 +- .../websession/lib/websession_templates.py | 12 ++--- modules/webstyle/css/invenio.css | 46 +++++++++--------- .../webstyle/img/youralerts_alert_dates.png | Bin 661 -> 0 bytes .../webstyle/img/youralerts_alert_search.png | Bin 803 -> 0 bytes .../yoursearches_search_last_performed.png | Bin 661 -> 0 bytes 7 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 modules/webstyle/img/youralerts_alert_dates.png delete mode 100644 modules/webstyle/img/youralerts_alert_search.png delete mode 100644 modules/webstyle/img/yoursearches_search_last_performed.png diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 26844b276e..215105c833 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -434,7 +434,7 @@ def tmpl_youralerts_display(self, 'alert_details_creation_last_run_dates': alert_details_creation_last_run_dates} out += """ -<table class="youralerts_display_table" cellspacing="2px"> +<table class="youralerts_display_table" cellspacing="0px"> <thead class="youralerts_display_table_header"> <tr> <td colspan="2"></td> diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 3d9fa0a9c7..2ffa14f645 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5242,7 +5242,7 @@ def tmpl_yoursearches_display(self, (CFG_SITE_SECURE_URL, paging_navigation[3], step, cgi.escape(p), ln, '/img/yoursearches_last_page.png') out += """ -<table class="websearch_yoursearches_table" cellspacing="2px"> +<table class="websearch_yoursearches_table" cellspacing="0px"> <thead class="websearch_yoursearches_table_header"> <tr> <td colspan="2"></td> diff --git a/modules/websession/lib/websession_templates.py b/modules/websession/lib/websession_templates.py index e0179d6ff9..91d89f4711 100644 --- a/modules/websession/lib/websession_templates.py +++ b/modules/websession/lib/websession_templates.py @@ -1476,18 +1476,18 @@ def tmpl_create_useractivities_menu(self, ln, selected, url_referer, guest, user 'ln' : ln, 'messages' : _('Your messages') } - if submitter: - out += '<li><a href="%(CFG_SITE_SECURE_URL)s/yoursubmissions.py?ln=%(ln)s">%(submissions)s</a></li>' % { - 'CFG_SITE_SECURE_URL' : CFG_SITE_SECURE_URL, - 'ln' : ln, - 'submissions' : _('Your submissions') - } if usealerts or guest: out += '<li><a href="%(CFG_SITE_SECURE_URL)s/yoursearches/display?ln=%(ln)s">%(searches)s</a></li>' % { 'CFG_SITE_SECURE_URL' : CFG_SITE_SECURE_URL, 'ln' : ln, 'searches' : _('Your searches') } + if submitter: + out += '<li><a href="%(CFG_SITE_SECURE_URL)s/yoursubmissions.py?ln=%(ln)s">%(submissions)s</a></li>' % { + 'CFG_SITE_SECURE_URL' : CFG_SITE_SECURE_URL, + 'ln' : ln, + 'submissions' : _('Your submissions') + } out += '</ul></div>' return out diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index f03c01ca69..2fdbeeb1f5 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2582,22 +2582,19 @@ a:hover.bibMergeImgClickable img { } .youralerts_display_table { - border: 1px solid #bbbbbb; + border: 1px solid #ffcc00; } .youralerts_display_table_header td { - background-color: #7a9bdd; + background-color: #ffffcc; padding: 2px; - border-bottom: 1px solid #3366cc; - border-right: 1px solid #3366cc; + border-bottom: 1px solid #ffcc00; } .youralerts_display_table_footer td { - background-color: #7a9bdd; - color: white; + background-color: #ffffcc; + color: black; padding: 2px; font-size: 80%; text-align: center; - border-bottom: 1px solid #3366cc; - border-right: 1px solid #3366cc; white-space: nowrap; } .youralerts_display_table_footer img { @@ -2618,16 +2615,18 @@ a:hover.bibMergeImgClickable img { vertical-align: bottom; } .youralerts_display_table_content { - background-color: #f2f2fa; + background-color: white; padding: 5px; text-align: left; vertical-align: top; + border-bottom: 1px solid #ffcc00; } .youralerts_display_table_content_mouseover { - background-color: #ebebfa; + background-color: #ffffe3; padding: 5px; text-align: left; vertical-align: top; + border-bottom: 1px solid #ffcc00; } .youralerts_display_table_content_container_left { float: left; @@ -2641,12 +2640,13 @@ a:hover.bibMergeImgClickable img { clear: both; } .youralerts_display_table_counter { - background-color: #f2f2fa; + background-color: white; padding: 5px 2px; text-align: right; vertical-align: top; font-size: 80%; color: gray; + border-bottom: 1px solid #ffcc00; } .youralerts_display_table_content_name { margin-bottom: 5px; @@ -2698,28 +2698,25 @@ a:hover.bibMergeImgClickable img { /* WebSearch module - YourSearches */ .websearch_yoursearches_table { - border: 1px solid #bbbbbb; + border: 1px solid #ffcc00; } .websearch_yoursearches_table_header td { - background-color: #7a9bdd; - color: white; + background-color: #ffffcc; + color: black; padding: 2px; font-weight: bold; font-size: 75%; text-transform: uppercase; - border-bottom: 1px solid #3366cc; - border-right: 1px solid #3366cc; + border-bottom: 1px solid #ffcc00; white-space: nowrap; vertical-align: top; } .websearch_yoursearches_table_footer td { - background-color: #7a9bdd; - color: white; + background-color: #ffffcc; + color: black; padding: 5px; font-size: 80%; text-align: center; - border-bottom: 1px solid #3366cc; - border-right: 1px solid #3366cc; white-space: nowrap; } .websearch_yoursearches_table_footer img { @@ -2740,24 +2737,27 @@ a:hover.bibMergeImgClickable img { vertical-align: bottom; } .websearch_yoursearches_table_content { - background-color: #f2f2fa; + background-color: white; padding: 5px; text-align: left; vertical-align: top; + border-bottom: 1px solid #ffcc00; } .websearch_yoursearches_table_content_mouseover { - background-color: #ebebfa; + background-color: #ffffe3; padding: 5px; text-align: left; vertical-align: top; + border-bottom: 1px solid #ffcc00; } .websearch_yoursearches_table_counter { - background-color: #f2f2fa; + background-color: white; padding: 5px 2px; text-align: right; vertical-align: top; font-size: 80%; color: grey; + border-bottom: 1px solid #ffcc00; } .websearch_yoursearches_table_content_options { margin: 5px; diff --git a/modules/webstyle/img/youralerts_alert_dates.png b/modules/webstyle/img/youralerts_alert_dates.png deleted file mode 100644 index b52ed5bb71c6937ab451a571eb6d476aae1e34bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 661 zcmV;G0&4w<P)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog80T1zq`SJh&0uo6?K~y-)b&@?xTVWW6uU3CT7L|gNP^GkF5uEgYxCw%gRa_*U zx|BkT{sTXj3<V)-iJ}%vM;)9BEg>Z4IEgqMPCUtbb!g7PXnVs8FVAy7pZ5cQkpLuT zXJ-OT0lWwyIB+wi+G@4zcDud+NYm7jkr8^m9wP(RCGZM3p<1nm5Q3^ek9NE5;^LyJ zQ4|Hn$H!G&UtbqE%PJjItJR>|>2y>b8ym~pxKz#ZZf<UxVQ+6wfDb-GMl6@h&d<;1 zSpa(b`};;w6c!d11iteG?|~S&0$vY!3Wb8IH#aw`)@ro?oMiqqj$^ZZWj!1n9R*;9 zot>S$!L6;Wp+<q9G@H%bFgiM#lUuEpfE0^GOQn+ie*f{b_WON-&j5!>lDN9MnrDjR z*vZMsp&iub=H`r|DAa1TzbcVSIfSo3Ix{ol?(VLQM#E;aX{A!JSS$*pz`;KQ3x$Gl z9Q*L_;L6I1s!5WVB#EkPYir)#-kKyyCiJ&lf$8aKRZFGP(4ZF=7uM_bAzQgzHceA! zXJ-XI0>q6*!zhaKC1hAyT5@@L*{8>HeSK}G(-8;&SCvX7C!}fW#>R%*+uP4B>~Vp^ z!$X1J0MkHve0&`4@9&=lJ^kO^-Fbd~9tKLheZ0uiiHQkMPftT8dK5*r+ig|5-LCa| vJq+~y0leftcmsR_egO_%1NZ{GdrJNT%MwrGizb5800000NkvXXu0mjf6N4YE diff --git a/modules/webstyle/img/youralerts_alert_search.png b/modules/webstyle/img/youralerts_alert_search.png deleted file mode 100644 index 742d87d0eea3ba912475ce8cca9909ff3ccdf13f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 803 zcmV+;1Kj+HP)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog5H!>o%n$G|L0-#AmK~y-)osVlsQ*juFfB$oiZo0YcY|5tQWois;n1Q57T4GX$ z%m}>X1#Id>G?bM5qS2Qikj$VP5>i<NMi;Zv%n$^fnMCQxT86g_5yFMLxUI9(hghhn zV9&>w=Y4oz@H`JM<ihO-1JLCaXaN8n^Hx0oAS3%koc7^6L%^M3JvL%}Q~tM*m7A~i z4ry8YV7VtgHM}}X8ayyGY)0d)`;YI=TT1~j>-MWM0umzSh$R4U{7OgrQ=&40fK-bt zSy%Nrx_`~IV_9*Y%tB;DDM>;I3$sx1sX!@+92_=&UY9iJPvumU>2m?d=Egetl-<x^ zJ4*H=+48ECrvcz>ZH<CbD78768BXKdQO8=5Txz*oaE{R^ZIr+&aNEUY<`<LKZrtqk z3e-n&h)xwGx9wF)$u3opbv8Docx{0Rpn#-cmS=@NWO?;BxaDu|wE{(%MjomCeCV`@ zU{3_&5tKwJvNt<AprO954uHJ5x%ql$edx%xFHUvpXL=gMW&j{tNt5?ZP7TfY_Im}= z+S-~w%}CO1V!4>}6@qrHLSnb9NPd3rKHt>%Y^xBdt*Q)MjgG(A!GxLIe*QKAMF>SG za6HeuJUm>BOFk`XPtlp8ymSDLjRBfgDC610LkAqJ%B*2UB!(3sN+v>>SV&YN0i=N} z_hD|)+H@o_=4D_1=*9q}mP@}%mS*j7v7w?+C2N!VFc8o*oE!&>o8!OCE%+r%Txr!6 z4Tm>-fgYcju#fchbnl}aA!_10?eDY;Wt@Y-!nDTE{60I0vKDHi)!{5F|6w#5O@OZ( z;ILB^rjDYiU%kbASG{apF2o-E55@6Fo$sn#Sb44IpVSAAWoZ#j3<d+*+S+y$syQl+ hJE$vb8ebGX{{y=9=$QvAQ2GD>002ovPDHLkV1msIXo&y- diff --git a/modules/webstyle/img/yoursearches_search_last_performed.png b/modules/webstyle/img/yoursearches_search_last_performed.png deleted file mode 100644 index b52ed5bb71c6937ab451a571eb6d476aae1e34bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 661 zcmV;G0&4w<P)<h;3K|Lk000e1NJLTq000mG000mO1^@s7X!Otb00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog80T1zq`SJh&0uo6?K~y-)b&@?xTVWW6uU3CT7L|gNP^GkF5uEgYxCw%gRa_*U zx|BkT{sTXj3<V)-iJ}%vM;)9BEg>Z4IEgqMPCUtbb!g7PXnVs8FVAy7pZ5cQkpLuT zXJ-OT0lWwyIB+wi+G@4zcDud+NYm7jkr8^m9wP(RCGZM3p<1nm5Q3^ek9NE5;^LyJ zQ4|Hn$H!G&UtbqE%PJjItJR>|>2y>b8ym~pxKz#ZZf<UxVQ+6wfDb-GMl6@h&d<;1 zSpa(b`};;w6c!d11iteG?|~S&0$vY!3Wb8IH#aw`)@ro?oMiqqj$^ZZWj!1n9R*;9 zot>S$!L6;Wp+<q9G@H%bFgiM#lUuEpfE0^GOQn+ie*f{b_WON-&j5!>lDN9MnrDjR z*vZMsp&iub=H`r|DAa1TzbcVSIfSo3Ix{ol?(VLQM#E;aX{A!JSS$*pz`;KQ3x$Gl z9Q*L_;L6I1s!5WVB#EkPYir)#-kKyyCiJ&lf$8aKRZFGP(4ZF=7uM_bAzQgzHceA! zXJ-XI0>q6*!zhaKC1hAyT5@@L*{8>HeSK}G(-8;&SCvX7C!}fW#>R%*+uP4B>~Vp^ z!$X1J0MkHve0&`4@9&=lJ^kO^-Fbd~9tKLheZ0uiiHQkMPftT8dK5*r+ig|5-LCa| vJq+~y0leftcmsR_egO_%1NZ{GdrJNT%MwrGizb5800000NkvXXu0mjf6N4YE From d67df23e7bf267df45a461915221057d449685b4 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 30 Aug 2011 16:34:56 +0200 Subject: [PATCH 56/83] WebSearch: more CSS fallback to default * Changes the arrows used for page navigation in the Your Searches page to match Invenio's default arrows. * Deletes the respective previously added images files. (closes #884) --- modules/websearch/lib/websearch_templates.py | 12 ++++++------ modules/webstyle/img/yoursearches_first_page.png | Bin 1568 -> 0 bytes modules/webstyle/img/yoursearches_last_page.png | Bin 1548 -> 0 bytes modules/webstyle/img/yoursearches_next_page.png | Bin 797 -> 0 bytes .../webstyle/img/yoursearches_previous_page.png | Bin 805 -> 0 bytes 5 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 modules/webstyle/img/yoursearches_first_page.png delete mode 100644 modules/webstyle/img/yoursearches_last_page.png delete mode 100644 modules/webstyle/img/yoursearches_next_page.png delete mode 100644 modules/webstyle/img/yoursearches_previous_page.png diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 2ffa14f645..90744d79e2 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5198,9 +5198,9 @@ def tmpl_yoursearches_display(self, (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Edit your existing alert(s)')) + \ '&nbsp;&nbsp;&nbsp;' + \ """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ - (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up a new alert')) or \ + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up as a new alert')) or \ """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ - (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up a new alert')) + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up as a new alert')) search_query_options = "%s&nbsp;&nbsp;&nbsp;%s" % \ (search_query_options_search, \ @@ -5224,10 +5224,10 @@ def tmpl_yoursearches_display(self, footer = '' if paging_navigation[0]: footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ - (CFG_SITE_SECURE_URL, 1, step, cgi.escape(p), ln, '/img/yoursearches_first_page.png') + (CFG_SITE_SECURE_URL, 1, step, cgi.escape(p), ln, '/img/sb.gif') if paging_navigation[1]: footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ - (CFG_SITE_SECURE_URL, page - 1, step, cgi.escape(p), ln, '/img/yoursearches_previous_page.png') + (CFG_SITE_SECURE_URL, page - 1, step, cgi.escape(p), ln, '/img/sp.gif') footer += "&nbsp;" displayed_searches_from = ((page - 1) * step) + 1 displayed_searches_to = paging_navigation[2] and (page * step) or nb_queries_distinct @@ -5236,10 +5236,10 @@ def tmpl_yoursearches_display(self, footer += "&nbsp;" if paging_navigation[2]: footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ - (CFG_SITE_SECURE_URL, page + 1, step, cgi.escape(p), ln, '/img/yoursearches_next_page.png') + (CFG_SITE_SECURE_URL, page + 1, step, cgi.escape(p), ln, '/img/sn.gif') if paging_navigation[3]: footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ - (CFG_SITE_SECURE_URL, paging_navigation[3], step, cgi.escape(p), ln, '/img/yoursearches_last_page.png') + (CFG_SITE_SECURE_URL, paging_navigation[3], step, cgi.escape(p), ln, '/img/se.gif') out += """ <table class="websearch_yoursearches_table" cellspacing="0px"> diff --git a/modules/webstyle/img/yoursearches_first_page.png b/modules/webstyle/img/yoursearches_first_page.png deleted file mode 100644 index a16d039aa93a3513f8d2116cf15d017f3cbe85ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1568 zcmV+*2H*LKP)<h;3K|Lk000e1NJLTq0012T000mO1^@s7oeD%p00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85Fvhu*DU}51)WJmK~y-)eN<^|R8<%~_s*L)GktHmJUT7&W;#<4DU_BjOqVHO z2U06o5Uq#>QILcfP+W1LkXEA-gw{}sh(r-lt5%H27Em;jAh-~gYH7iUG*r6pLpz=2 zef}twLWt+r&G~Zg{c_IvKE%Yv!~+25JO%*x)T!>o*7g>9Y{}w1z1|)|Qc~hQxDHE< z#mOB``++`hFSWL{XftPel8~9{n&ZyNx{{OQ&S~#x)v~g)5;8JeDV^ONl$w?@m*4-u zyg;@)gHD_{dBQ&!V5Oy{gTMWD>RfkMx3+N6!+toNafP@6{fM&;E$vMN0NZzLr}mCk z0)S0r8>y|mg;<}rmjb~67-Qg!BNz;V>o^z~_@I9fTQ*m+k3QV#A<{5PiDuQGeK8pz zATe>mea$WZP_{es%837%JIDQ$&o@9eyKMphsHzG8NJ>sx+}zwm<Hn7hCs<WAWHL$M zobhL$Th&oswpr;PxCSDEZ_o#Kw%c!sQU0#nvMt_wtq%Yp5DY-I*+P8c#DtI}$#6Iw zp=C>-a8&R4yh$$z5JdwdNk(2?9=%>#maucz`<IOd1B?a}A|n(`nUWU5$5p%XTUwfF zWBK~V5p9Er_||tTzOApXa}0Burh$l9#n$pqKCk{XA7k$rQyAy47XlD)3lb)bzYDj) zm@&>Q)YQ}v5fK1BT!?en<4Q```KqcurI8mDB`W&Gmsa<juRBiyFjP~c@1*q1$vkS5 z@xbogpUm#)YQv#}hXDXNZV$7n%6ohEe6_rzs|{Bhn;_8jHxpAG1)IuW-*2<2)aUJm zUJwBgJf7VAl9F{t($dqYr?(rN>4yBPsi~pl<Yb&VbK2*KcNiIC&<g?phNDN1LQxcN za!Rrc!hd|@l}2#AIHXBhTDqxt&C6vB96Bygm`Q@cAi|S7!(3dv=IKySgN_SiGKE3V z>w!y`{u&l%*a~dlvGw<UUqAhL?1zy&D}Q^#sR3U<U2gcBgg$RCy1F`VWREcbNOrlh z)b`G{e@5^Gz_obs<B@gswG9|Qe%!qM`}a|OeH}G4G|<#(?yDoMxeavSe9W}z?qg4_ zT1k>DCF1|V0K`slrG8oZYKb=?;m&zHOp-LGGagY&BpeP0G))TufXCyx>7&d{*Q2YR zUU})l#rj}Rcju+Od%w9WD>EDM&iK&n8cRz}xwmNHLl@4TJrnBb?P@-9<lFfV7S5M+ zf<S^S%S0gP_4?~l0B^kZ7CZUN@yLvf^vx?)J~_9bV2)yzW%vVr8q!&VPA7msh>NpR zZEY>)7cMk5G+rzzDq6U#sHiA5Dk=(ozn=gwB7p0300^=yYl1Ax8i*hoi~s<m$z=89 z<@7)E!U}QF?}HZ7z&XQ!um8F^1ZV_}2nK@@=5X@|2M+9WO`ks97zhOr2n2|84)3)- zFvb|b03w965IE;h6oq(9jD-Y2fL=EgLzBtWTvhd1er4sBKbxCc5Up6C)9FYr4s8TE z%#3hz1j5bXFc|d6%Fce|>%Cv+Ra92|T66Aq#9FLO6h$%`Ob9bcFw5bH2#-WWcoYOd zfNCAB33j`UL{UT_7=S1mV6~3cw!O3M7=ZD`Yt~`?hBqe~jpEBrNBok)2j=72fR~_w zan2!065?!jzWVvqKLN-hB5W$#7_U)qx!q<jeq_;O7#QqFAP@p)9C}d?dz_sF)uw8q z!GO;0c1U3|Mq6WVN<{H1YwG|kC8DMK4t=AvHebzGl;|z9XBAjvvy5<a1dSY->gwvt z3l=O`OGIl={``}4?%e5IizT)qH+RN3lhFjLY9)5Q?vK&6=l@6zX(1vaL`Fsq%(}ba zIAct^HIlWpwU{(%(tmsP@OKAYDM{%UR8{S{-K-GN@a%$!@ZI-^Cy!Drt^WevHBK#s Snf#mp0000<MNUMnLSTa2;n+I> diff --git a/modules/webstyle/img/yoursearches_last_page.png b/modules/webstyle/img/yoursearches_last_page.png deleted file mode 100644 index c61494c785551a39a17c19ea3dcd458e53aeec0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1548 zcmV+n2J`ueP)<h;3K|Lk000e1NJLTq0012T000mO1^@s7oeD%p00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85D+g`*B1Z)1&K*SK~y-)g;Q;8Q)Lu>-nVx{-B{UR182g5kq<Wzh-3(&5E0e_ zI*niuK?JA72__nTm}s0ad>CO93Cac<2TJ&u7!`E5P5BbWq7Eo!>pIwM*WT`nj^5te z-uB+U#~&M7NsQ00lPAwP?>WyoFaBu$Z48D`TX+0X*z84%13iAxKhQ^yJwDF_0LZdz z1%Q@T=NR<e=%r=PE<M}Z*G)Ei(IU&cWpCSRe>%}_wOaR;l$4C9-v31~eR%qjarfpm z+iXRx$S=76$xD|m3IHf5D45vhb`b!~nmw~?+VrVUfQW820V3kJ;zKnDilAvKeRW_T z0YD_8(a~>e2mn+qOsu<CCeAsAr3^#Nh{M7;Qd5V4aSrs~=%wk?rY-^o<HEujtLDv{ zho0U5VRzN8qQ3rK+FiMG5fS0=;llucrlt#dk*G#{KmT-ja$oP1z2)`#J89RaA1}n( zwX5w>JxY2^CnX#rAq2g@>Aj}y72C(^u_0bnNF)-pW$PyQ;wS9BSWKrER<E>k=hYTV zRF6Q{qmU#CAML2<aWuEhZK$iiBN~kk^4x;z{nf#9XPXKKf<e+EDmpqkEtoiQeDQ-b zrquvoNJ3hATK=3zW}OCLN#-A(GfQ^4T4}<B`-)N5Q2W4;$H^Mh)*UYd0H@RWuRR<* z@>NmI(L**S%YHiF`0KUY+<T_Yo;63iC2DLsN3!hilTuSB6)%1&n3PQ3E-!Gs;~Irk zg@}kg_;B;)TQAo){6wl6CL*GW9owpt6WiO}1edFo;_(=15tRl)H^eU|f2Nm8);#>} zci#;08V%gMA)HsQQ0Z%>bBTzqUUiaWnkGWZO@IMH2=>6#DTruKH)xS4lyC@)bC3{h z%-y50V#RZH0AQLXxDWy<DXB=2Qpq$Du-oigmn?bm&R9%`BuTJv3ndZ+dy#G5sfP1+ z0RR{S91aJClmS})+_JW0`@j5hKZTT=wBhyAjwGHF5jh+VLfL!ossKjgHmu)JO5l9l zZ8Z0IJap{X_rwkzIk3=bwbr=X+m0=NcIEn<?3@r|jD!&2oZns#E?4VVXRE{0-Q8XP z(9GE{J~(|wH)BlTKk#As7GKGlb@v#Cf$p9F9L~!{+~e`E`zIA-tX{R|`HYO=1D$_% znB^58{JwWj)f6IPw-@Q??Bt`evaxc-ss;J^1^z%lPE_vMb!Eqn?Q@e4u&k^M01%L6 zk(rqVONs@<(=*WJ>0)@}&DZVjb~hQOL569Nu187JRH5q;>h0^LJ)c!}t}A_cLw}Ew z3ILUrmE(T+@faDVL8fVv9@GEU2M2<5@bCfom33>kIW9ZXhzQG9Eb|f(nF*6xT`jbJ z!>cw)mIK^03}~7<XeR?e#NwuaMdEm>`02c7mM+~15#M?0^hve4`iqM>ql*!VXkd(i zF$N;R;-&yWVE+8a?_RKA;YQB+#)}u5btM#%03aHTz=)gh`(-W#0^Q6sO&Dez7A}Ev z27rNqfip(d%#rNOnNz6w`pBQHEp6*Yjmo~Js_ONB|78ZrB7vBo5gD1x?QTQWp4~y` zm6p|!s9FUeR|o+jf}(_(q$tYZ-Utws0LX$&YbLgrZ^MPg3!izt-cM4bbjSI|Qv?7r zADTH4U@%Q%$Q97YtZY`bw+iRZo;`8>ddH@$5xEzR|9Dj7jvCMMCS(Z!(Bn}s280!b zNoqLErIZvTOo8%>a;3KR#A~wOe<Bzh=m&t@++4`A{P#>gpN~lv3)0huW6RdfvHJR- zH~M^C-*tI?zQNZ2f!yED#*Qx>oGBpyFsQ1^S$0l#UV7SHV>&xMzXM3%f517<89(8^ yNo_87BLL%n<kIBH`SU}HlExT=BuP+Jt@$t8`Y@C_t857X0000<MNUMnLSTYrdE+1e diff --git a/modules/webstyle/img/yoursearches_next_page.png b/modules/webstyle/img/yoursearches_next_page.png deleted file mode 100644 index c12bad413971c3258a23eeba31e611aace9cd6f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 797 zcmV+&1LFLNP)<h;3K|Lk000e1NJLTq000dD000mO1^@s7x}?Rg00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85HFc9`YHeb0-8xgK~yNujgi|+({UKapZELwI$CCS@Bp<ASjsF)5L*2K9%j@< z7tvKTH3_=urn?S9=wMhBO~Q;$M2ZCkyJ#6jUKFBpYC0*~@4~IM+03oALv26zd%K9l zqM*<1^FBNe?}L%i;ZguV!)~iJ{OKbD03wk{K4#}4+|+CvguV4-UFuts&$pd5JDtu_ zY}s15XL8cZ0AMzoO<?}%0|1dur?~fF4+8+rM8Yft!MJ28q*N5t*VP|jrU&Tkx>gg7 z&2i^-$G*jWuh&~5+=Fh#Xj}@Lt>sZ7`IU7&$LHG4_*_q22I-yjD&M}RAxOYrFyQ8` zo6-IkuXZYujHsyCymyDC+6h2l5mL(ac=p^?1>m>706_l0G`oif1BJ^Ax7XHK;|t2W zskfX+er0Cni<d5REePY|Z#bLHa4MbR*q1qd!aKoD%{D7}JRV-Z!2}RN*K-g=ct54; zQrC0H%QFA~XTIyyuxIz(EnCetxZOh>PkiC#!;LBcWg+9#=@V%HYw-uv`>?yFpkUc! zpXzfpSoa<+-&8(FM2vqB3<iruz#pKB&6Udz9c<jc&SaYX@FAc(IxfF)T)nc3nd#TD z-EIc}sH#db9G(%GOa}S+1pt7O(h@W^wro=rF%XSKalhwY=w<)Q?y9QlRshNA>1mRo zP>3{5Ln@Vo5CTL5W}UM0IzWut+PXFMR_k#=;`qpjXa3IJ+ru&(o*_-spv`BY2nitw z0D(XdM7+FkCHZ_lblvETjg5{US-ontTgKz@pDQz<a}KN%R;?&P$2A8AUJp1`Rc+I< zVrX=1PzL~u#bT0)M1rKugCGU%?d_?)zP@9@VDQOo<a79M-}C$Zgref24P|8;cL9+2 bKUV$#Ae=X4gvQ*!00000NkvXXu0mjf?67Wo diff --git a/modules/webstyle/img/yoursearches_previous_page.png b/modules/webstyle/img/yoursearches_previous_page.png deleted file mode 100644 index b3bdbd05bec2e215a05b2b710fa070e3604ada6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 805 zcmV+=1KRwFP)<h;3K|Lk000e1NJLTq000dD000mO1^@s7x}?Rg00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z+Xog85IJq+PkR6W0-{MoK~yNub&xwuQ*ji=zyH0rP#&dF`k+{fMF$W<p@m9<4<rx= zpo>bBO<IG4(Zod^9gIX24Uff$8&QlghCv}p!e&AvMAH%;1tXx8wzmb_0-^LC2b3Ve zZ#p?SkMH{dKo$Vt`i*O!BGF$208nex`Mf{4LetYz!?4+G2Ai$2N(=|6tlZpy(a}*Y z04ywgr;dm1_1Gf;0MOmrMS<W7vCyhWj4?1U@G0`tyWW}UVRM<;37t-tw|BAJQsxlF zARRec&?ua!uIBUd4~04&Jj#l##{dxG+i@5Sh6LASw{M&}S!aCpdf=x#IT><!3Nn={ zSgjU`EiEl+3<g6003=BQV~ln6b`Ip|^iOe+3JMNa{nOBB)%x3aZbc_1CU$6)mmTL? zS}wgMA_|85<Z_J>YHChjn4I)bcvX~E*TNJEi{x^R5gQ->95<Sb0t16g5CAaD&&}`L zcDA*4h#`?;v8WW^j+4jZrrpOx#JYPPf7#evC$D#w*nX=s-Wj#mJN1nh1i>gt(i;9R z4nS_D&C>61wD$Rym%pO0u+RViR;zXIAaAvnH#?k{y&jL7)*`DE6n7#XD=umjOePcA zsx0vR2&pP6EuC#`SDWljc9kMSfvv4A0sxW-9M1y)6pD07(C6yjI9i<*rRGv@b8{Wr zi3GqAqOk}V<Jh0HNJtVmj)zK>EpbIf#pn8-KWpgj>GJw~0q8V2NRlOyJUIoaDQQro zXF!>e31vnmWHJG{dOfkh!9ktfZeRHu=6?IVLYYjsnP)U!X+D1u>zgr12?-qIu(-5{ z7ccwIGPm2UEh#DS??*;N_&74GoSU1iRco|QYR}XeBasN+5522re!o9WtJTI1VogL~ jjP2GRMuyDlY_0D%<{TiF_>Qx&00000NkvXXu0mjfk8WAP From d4acf59156b6792fd869f2cb1324e12eca746a23 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Tue, 30 Aug 2011 17:25:59 +0200 Subject: [PATCH 57/83] WebAlert: paging for youralerts * Introduces page navigation support for the "Your Alerts" page. (closes #885) --- modules/webalert/lib/webalert.py | 69 ++++++++++++++++--- modules/webalert/lib/webalert_templates.py | 35 ++++++++-- modules/webalert/lib/webalert_webinterface.py | 8 ++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 127a76a3c9..3686ab94e0 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -35,6 +35,8 @@ import invenio.template webalert_templates = invenio.template.load('webalert') +CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS = 20 + ### IMPLEMENTATION class AlertError(Exception): @@ -173,11 +175,13 @@ def perform_add_alert(alert_name, frequency, notification, run_sql(query, params) out = _("The alert %s has been added to your profile.") out %= '<b>' + cgi.escape(alert_name) + '</b>' - out += perform_request_youralerts_display(uid, idq=None, ln=ln) + out += perform_request_youralerts_display(uid, idq=0, ln=ln) return out def perform_request_youralerts_display(uid, - idq=None, + idq=0, + page=1, + step=CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS, ln=CFG_SITE_LANG): """ Display a list of the user defined alerts. If a specific query id is defined @@ -189,6 +193,12 @@ def perform_request_youralerts_display(uid, @param idq: The specified query id for which to display the user alerts @type idq: int + @param page: + @type page: integer + + @param step: + @type step: integer + @param ln: The interface language @type ln: string @@ -203,6 +213,42 @@ def perform_request_youralerts_display(uid, else: idq_clause = "" + query_nb_alerts = """ SELECT COUNT(*) + FROM user_query_basket AS uqb + WHERE uqb.id_user=%%s + %s""" % (idq_clause,) + params_nb_alerts = (uid,) + result_nb_alerts = run_sql(query_nb_alerts, params_nb_alerts) + nb_alerts = result_nb_alerts[0][0] + + # The real page starts counting from 0, i.e. minus 1 from the human page + real_page = page - 1 + # The step needs to be a positive integer + if (step <= 0): + step = CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS + # The maximum real page is the integer division of the total number of + # searches and the searches displayed per page + max_real_page = (nb_alerts / step) - (not (nb_alerts % step) and 1 or 0) + # Check if the selected real page exceeds the maximum real page and reset + # if needed + if (real_page >= max_real_page): + #if ((nb_queries_distinct % step) != 0): + # real_page = max_real_page + #else: + # real_page = max_real_page - 1 + real_page = max_real_page + page = real_page + 1 + elif (real_page < 0): + real_page = 0 + page = 1 + # Calculate the start value for the SQL LIMIT constraint + limit_start = real_page * step + # Calculate the display of the paging navigation arrows for the template + paging_navigation = (real_page >= 2, + real_page >= 1, + real_page <= (max_real_page - 1), + (real_page <= (max_real_page - 2)) and (max_real_page + 1)) + # query the database query = """ SELECT q.id, q.urlargs, @@ -220,10 +266,11 @@ def perform_request_youralerts_display(uid, ON uqb.id_basket=bsk.id WHERE uqb.id_user=%%s %s - ORDER BY uqb.alert_name ASC""" % ('%%Y-%%m-%%d %%H:%%i:%%s', - '%%Y-%%m-%%d %%H:%%i:%%s', - idq_clause,) - params = (uid,) + ORDER BY uqb.alert_name ASC + LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', + '%%Y-%%m-%%d %%H:%%i:%%s', + idq_clause,) + params = (uid, limit_start, step) result = run_sql(query, params) alerts = [] @@ -261,8 +308,12 @@ def perform_request_youralerts_display(uid, # link to the "add new alert" form out = webalert_templates.tmpl_youralerts_display(ln=ln, alerts=alerts, - guest=isGuestUser(uid), + nb_alerts=nb_alerts, idq=idq, + page=page, + step=step, + paging_navigation=paging_navigation, + guest=isGuestUser(uid), guesttxt=warning_guest_user(type="alerts", ln=ln)) return out @@ -291,7 +342,7 @@ def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG) out += "The alert <b>%s</b> has been removed from your profile.<br /><br />\n" % cgi.escape(alert_name) else: out += "Unable to remove alert <b>%s</b>.<br /><br />\n" % cgi.escape(alert_name) - out += perform_request_youralerts_display(uid, idq=None, ln=ln) + out += perform_request_youralerts_display(uid, idq=0, ln=ln) return out @@ -351,7 +402,7 @@ def perform_update_alert(alert_name, frequency, notification, id_basket, id_quer run_sql(query, params) out += _("The alert %s has been successfully updated.") % ("<b>" + cgi.escape(alert_name) + "</b>",) - out += "<br /><br />\n" + perform_request_youralerts_display(uid, idq=None, ln=ln) + out += "<br /><br />\n" + perform_request_youralerts_display(uid, idq=0, ln=ln) return out def is_selected(var, fld): diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 215105c833..3bfef36ce1 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -264,7 +264,11 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio def tmpl_youralerts_display(self, ln, alerts, + nb_alerts, idq, + page, + step, + paging_navigation, guest, guesttxt): """ @@ -325,14 +329,14 @@ def tmpl_youralerts_display(self, # Diplay a message about the number of alerts. if idq: msg = _('You have defined %(number_of_alerts)s alerts based on that search query.') % \ - {'number_of_alerts': '<strong>' + str(len(alerts)) + '</strong>'} + {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define a new one')), 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} else: msg = _('You have defined a total of %(number_of_alerts)s alerts.') % \ - {'number_of_alerts': '<strong>' + str(len(alerts)) + '</strong>'} + {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), @@ -340,7 +344,7 @@ def tmpl_youralerts_display(self, 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} out = '<p>' + msg + '</p>' - counter = 0 + counter = (page - 1) * step youralerts_display_html = "" for alert in alerts: counter += 1 @@ -433,6 +437,26 @@ def tmpl_youralerts_display(self, 'alert_details_options': alert_details_options, 'alert_details_creation_last_run_dates': alert_details_creation_last_run_dates} + footer = '' + if paging_navigation[0]: + footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, 1, step, idq, ln, '/img/sb.gif') + if paging_navigation[1]: + footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, page - 1, step, idq, ln, '/img/sp.gif') + footer += "&nbsp;" + displayed_alerts_from = ((page - 1) * step) + 1 + displayed_alerts_to = paging_navigation[2] and (page * step) or nb_alerts + footer += _('Displaying alerts <strong>%i to %i</strong> from <strong>%i</strong> total alerts') % \ + (displayed_alerts_from, displayed_alerts_to, nb_alerts) + footer += "&nbsp;" + if paging_navigation[2]: + footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, page + 1, step, idq, ln, '/img/sn.gif') + if paging_navigation[3]: + footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + (CFG_SITE_SECURE_URL, paging_navigation[3], step, idq, ln, '/img/se.gif') + out += """ <table class="youralerts_display_table" cellspacing="0px"> <thead class="youralerts_display_table_header"> @@ -442,13 +466,14 @@ def tmpl_youralerts_display(self, </thead> <tfoot class="youralerts_display_table_footer"> <tr> - <td colspan="2"></td> + <td colspan="2">%(footer)s</td> </tr> </tfoot> <tbody> %(youralerts_display_html)s </tbody> -</table>""" % {'youralerts_display_html': youralerts_display_html} +</table>""" % {'footer': footer, + 'youralerts_display_html': youralerts_display_html} return out diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index a189e9c7fd..419f41b0f9 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -239,8 +239,10 @@ def modify(self, req, form): def display(self, req, form): - argd = wash_urlargd(form, {'idq': (int, None), - }) + argd = wash_urlargd(form, {'idq': (int, 0), + 'page': (int, 1), + 'step': (int, 20), + 'ln': (str, '')}) uid = getUid(req) @@ -277,6 +279,8 @@ def display(self, req, form): return page(title=_("Your Alerts"), body=perform_request_youralerts_display(uid, idq=argd['idq'], + page=argd['page'], + step=argd['step'], ln=argd['ln']), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % \ {'sitesecureurl' : CFG_SITE_SECURE_URL, From 8d50bac3dacc174844c58a1732878dfcc87f8dd0 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 1 Sep 2011 18:10:42 +0200 Subject: [PATCH 58/83] WebAlert: Searching for youralerts * Introduces searching in the user's defined alerts based on the alerts' names and arguments. * Fixes small bug in the paging navigation that caused a crash when no alerts were to be displayed. * Adds user confirm step before permanently deleting an alert. (closes #886) --- modules/webalert/lib/webalert.py | 201 ++++++++++-------- modules/webalert/lib/webalert_templates.py | 81 ++++++- modules/webalert/lib/webalert_webinterface.py | 2 + 3 files changed, 191 insertions(+), 93 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 3686ab94e0..3b19d226d5 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -21,6 +21,7 @@ import cgi import time +from urllib import quote from invenio.config import CFG_SITE_LANG from invenio.dbquery import run_sql @@ -182,6 +183,7 @@ def perform_request_youralerts_display(uid, idq=0, page=1, step=CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS, + p='', ln=CFG_SITE_LANG): """ Display a list of the user defined alerts. If a specific query id is defined @@ -209,110 +211,139 @@ def perform_request_youralerts_display(uid, out = "" if idq: - idq_clause = "AND uqb.id_query=%i" % (idq,) + idq_clause = "q.id=%i" % (idq,) else: idq_clause = "" - query_nb_alerts = """ SELECT COUNT(*) + search_clause = "" + search_clause_urlargs = [] + search_clause_alert_name = [] + if p: + p_stripped = p.strip() + p_stripped_args = p.split() + sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg).replace('%','%%') + '%%\'' for p_stripped_arg in p_stripped_args] + for sql_p_stripped_arg in sql_p_stripped_args: + search_clause_urlargs.append("q.urlargs LIKE %s" % (sql_p_stripped_arg,)) + search_clause_alert_name.append("uqb.alert_name LIKE %s" % (sql_p_stripped_arg,)) + search_clause = "((%s) OR (%s))" % (" AND ".join(search_clause_urlargs), + " AND ".join(search_clause_alert_name)) + + idq_and_search_clause_list = [clause for clause in (idq_clause, search_clause) if clause] + idq_and_search_clause = ' AND '.join(idq_and_search_clause_list) + + query_nb_alerts = """ SELECT COUNT(IF((uqb.id_user=%%s + %s),uqb.id_query,NULL)), + COUNT(q.id) FROM user_query_basket AS uqb - WHERE uqb.id_user=%%s - %s""" % (idq_clause,) + RIGHT JOIN query AS q + ON uqb.id_query=q.id + %s""" % ((search_clause and ' AND ' + search_clause), + (idq_clause and ' WHERE ' + idq_clause)) params_nb_alerts = (uid,) result_nb_alerts = run_sql(query_nb_alerts, params_nb_alerts) nb_alerts = result_nb_alerts[0][0] - - # The real page starts counting from 0, i.e. minus 1 from the human page - real_page = page - 1 - # The step needs to be a positive integer - if (step <= 0): - step = CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS - # The maximum real page is the integer division of the total number of - # searches and the searches displayed per page - max_real_page = (nb_alerts / step) - (not (nb_alerts % step) and 1 or 0) - # Check if the selected real page exceeds the maximum real page and reset - # if needed - if (real_page >= max_real_page): - #if ((nb_queries_distinct % step) != 0): - # real_page = max_real_page - #else: - # real_page = max_real_page - 1 - real_page = max_real_page - page = real_page + 1 - elif (real_page < 0): - real_page = 0 - page = 1 - # Calculate the start value for the SQL LIMIT constraint - limit_start = real_page * step - # Calculate the display of the paging navigation arrows for the template - paging_navigation = (real_page >= 2, - real_page >= 1, - real_page <= (max_real_page - 1), - (real_page <= (max_real_page - 2)) and (max_real_page + 1)) - - # query the database - query = """ SELECT q.id, - q.urlargs, - uqb.id_basket, - bsk.name, - uqb.alert_name, - uqb.frequency, - uqb.notification, - DATE_FORMAT(uqb.date_creation,'%s'), - DATE_FORMAT(uqb.date_lastrun,'%s') - FROM user_query_basket uqb - LEFT JOIN query q - ON uqb.id_query=q.id - LEFT JOIN bskBASKET bsk - ON uqb.id_basket=bsk.id - WHERE uqb.id_user=%%s - %s - ORDER BY uqb.alert_name ASC - LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', - '%%Y-%%m-%%d %%H:%%i:%%s', - idq_clause,) - params = (uid, limit_start, step) - result = run_sql(query, params) - - alerts = [] - for (query_id, - query_args, - bsk_id, - bsk_name, - alert_name, - alert_frequency, - alert_notification, - alert_creation, - alert_last_run) in result: - try: - if not query_id: - raise StandardError("""\ + nb_queries = result_nb_alerts[0][1] + + # In case we do have some alerts, proceed with the needed calculations and + # fetching them from the database + if nb_alerts: + # The real page starts counting from 0, i.e. minus 1 from the human page + real_page = page - 1 + # The step needs to be a positive integer + if (step <= 0): + step = CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS + # The maximum real page is the integer division of the total number of + # searches and the searches displayed per page + max_real_page = nb_alerts and ((nb_alerts / step) - (not (nb_alerts % step) and 1 or 0)) or 0 + # Check if the selected real page exceeds the maximum real page and reset + # if needed + if (real_page >= max_real_page): + #if ((nb_queries_distinct % step) != 0): + # real_page = max_real_page + #else: + # real_page = max_real_page - 1 + real_page = max_real_page + page = real_page + 1 + elif (real_page < 0): + real_page = 0 + page = 1 + # Calculate the start value for the SQL LIMIT constraint + limit_start = real_page * step + # Calculate the display of the paging navigation arrows for the template + paging_navigation = (real_page >= 2, + real_page >= 1, + real_page <= (max_real_page - 1), + (real_page <= (max_real_page - 2)) and (max_real_page + 1)) + + query = """ SELECT q.id, + q.urlargs, + uqb.id_basket, + bsk.name, + uqb.alert_name, + uqb.frequency, + uqb.notification, + DATE_FORMAT(uqb.date_creation,'%s'), + DATE_FORMAT(uqb.date_lastrun,'%s') + FROM user_query_basket uqb + LEFT JOIN query q + ON uqb.id_query=q.id + LEFT JOIN bskBASKET bsk + ON uqb.id_basket=bsk.id + WHERE uqb.id_user=%%s + %s + %s + ORDER BY uqb.alert_name ASC + LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', + '%%Y-%%m-%%d %%H:%%i:%%s', + (idq_clause and ' AND ' + idq_clause), + (search_clause and ' AND ' + search_clause)) + params = (uid, limit_start, step) + result = run_sql(query, params) + + alerts = [] + for (query_id, + query_args, + bsk_id, + bsk_name, + alert_name, + alert_frequency, + alert_notification, + alert_creation, + alert_last_run) in result: + try: + if not query_id: + raise StandardError("""\ Warning: I have detected a bad alert for user id %d. It seems one of his/her alert queries was deleted from the 'query' table. Please check this and delete it if needed. Otherwise no problem, I'm continuing with the other alerts now. Here are all the alerts defined by this user: %s""" % (uid, repr(result))) - alerts.append({'queryid' : query_id, - 'queryargs' : query_args, - 'textargs' : get_textual_query_info_from_urlargs(query_args, ln=ln), - 'userid' : uid, - 'basketid' : bsk_id, - 'basketname' : bsk_name, - 'alertname' : alert_name, - 'frequency' : alert_frequency, - 'notification' : alert_notification, - 'created' : alert_creation, - 'lastrun' : alert_last_run}) - except StandardError: - register_exception(alert_admin=True) - - # link to the "add new alert" form + alerts.append({'queryid' : query_id, + 'queryargs' : query_args, + 'textargs' : get_textual_query_info_from_urlargs(query_args, ln=ln), + 'userid' : uid, + 'basketid' : bsk_id, + 'basketname' : bsk_name, + 'alertname' : alert_name, + 'frequency' : alert_frequency, + 'notification' : alert_notification, + 'created' : alert_creation, + 'lastrun' : alert_last_run}) + except StandardError: + register_exception(alert_admin=True) + else: + alerts = [] + paging_navigation = () + out = webalert_templates.tmpl_youralerts_display(ln=ln, alerts=alerts, nb_alerts=nb_alerts, + nb_queries=nb_queries, idq=idq, page=page, step=step, paging_navigation=paging_navigation, + p=p, guest=isGuestUser(uid), guesttxt=warning_guest_user(type="alerts", ln=ln)) return out diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 3bfef36ce1..b08cc98b36 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -265,10 +265,12 @@ def tmpl_youralerts_display(self, ln, alerts, nb_alerts, + nb_queries, idq, page, step, paging_navigation, + p, guest, guesttxt): """ @@ -309,13 +311,44 @@ def tmpl_youralerts_display(self, # In case the user has not yet defined any alerts display only the # following message - if not alerts: - if idq: - msg = _('You have not defined any alerts yet based on that search query.') + if not nb_alerts: + if idq and not p: + if nb_queries: + msg = _('You have not defined any alerts yet based on that search query.') + msg += "<br />" + msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ + {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define one now')), + 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + else: + msg = _('The selected search query seems to be invalid.') + msg += "<br />" + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} + elif p and not idq: + msg = _('You have not defined any alerts yet including the terms %s.') % \ + ('<strong>' + cgi.escape(p) + '</strong>',) msg += "<br />" - msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ - {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define one now')), - 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} + elif p and idq: + if nb_queries: + msg = _('You have not defined any alerts yet based on that search query including the terms %s.') % \ + ('<strong>' + cgi.escape(p) + '</strong>',) + msg += "<br />" + msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ + {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define one now')), + 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + else: + msg = _('The selected search query seems to be invalid.') + msg += "<br />" + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} else: msg = _('You have not defined any alerts yet.') msg += '<br />' @@ -327,13 +360,30 @@ def tmpl_youralerts_display(self, return out # Diplay a message about the number of alerts. - if idq: + if idq and not p: msg = _('You have defined %(number_of_alerts)s alerts based on that search query.') % \ {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define a new one')), 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} + elif p and not idq: + msg = _('You have defined %(number_of_alerts)s alerts including the terms %(p)s.') % \ + {'p': '<strong>' + cgi.escape(p) + '</strong>', + 'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} + msg += '<br />' + msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), + 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} + elif idq and p: + msg = _('You have defined %(number_of_alerts)s alerts based on that search query including the terms %(p)s.') % \ + {'p': '<strong>' + cgi.escape(p) + '</strong>', + 'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} + msg += '<br />' + msg += _('You may want to %(new_alert)s or display all %(youralerts)s.') % \ + {'new_alert': '<a href="%s/youralerts/input?ln=%s&amp;idq=%i">%s</a>' % (CFG_SITE_SECURE_URL, ln, idq, _('define a new one')), + 'youralerts': '<a href="%s/youralerts/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your alerts'))} else: msg = _('You have defined a total of %(number_of_alerts)s alerts.') % \ {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} @@ -344,6 +394,19 @@ def tmpl_youralerts_display(self, 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} out = '<p>' + msg + '</p>' + # Search form + search_form = """ + <form name="youralerts_search" action="%(action)s" method="get"> + <small><strong>%(search_text)s</strong></small> + <input name="p" value="%(p)s" type="text" /> + <input class="formbutton" type="submit" value="%(submit_label)s" /> + </form> + """ % {'search_text': _('Search all your alerts for'), + 'action': '%s/youralerts/display?ln=%s' % (CFG_SITE_SECURE_URL, ln), + 'p': cgi.escape(p), + 'submit_label': _('Search')} + out += '<p>' + search_form + '</p>' + counter = (page - 1) * step youralerts_display_html = "" for alert in alerts: @@ -406,7 +469,9 @@ def tmpl_youralerts_display(self, 'idq' : alert_query_id, 'name' : alert_name, 'idb' : alert_basket_id}, - _('Delete')) + _('Delete'), + {'onclick': 'return confirm(\'%s\')' % \ + (_('Are you sure you want to permanently delete this alert?'),)}) alert_details_options = '<img src="%s/img/youralerts_alert_edit.png" />' % (CFG_SITE_URL,) + \ alert_details_options_edit + \ '&nbsp;&nbsp;&nbsp;' + \ diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index 419f41b0f9..6c2c850d99 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -242,6 +242,7 @@ def display(self, req, form): argd = wash_urlargd(form, {'idq': (int, 0), 'page': (int, 1), 'step': (int, 20), + 'p': (str, ''), 'ln': (str, '')}) uid = getUid(req) @@ -281,6 +282,7 @@ def display(self, req, form): idq=argd['idq'], page=argd['page'], step=argd['step'], + p=argd['p'], ln=argd['ln']), navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % \ {'sitesecureurl' : CFG_SITE_SECURE_URL, From 77354e037d7b695df92af3716456da7bfacb5fc8 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 1 Sep 2011 18:26:28 +0200 Subject: [PATCH 59/83] WebSearch: Fix paging bug in yoursearches * Fixes small bug in the paging navigation that caused a crash when no searches were to be displayed. * Introduces fix that skips querying the database again when there are no matching user searches. (closes #887) --- modules/webalert/lib/webalert.py | 2 +- modules/websearch/lib/websearch_templates.py | 2 +- .../websearch/lib/websearch_yoursearches.py | 109 +++++++++--------- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 3b19d226d5..eda714e9b5 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -254,7 +254,7 @@ def perform_request_youralerts_display(uid, step = CFG_WEBALERT_YOURALERTS_MAX_NUMBER_OF_DISPLAYED_ALERTS # The maximum real page is the integer division of the total number of # searches and the searches displayed per page - max_real_page = nb_alerts and ((nb_alerts / step) - (not (nb_alerts % step) and 1 or 0)) or 0 + max_real_page = nb_alerts and ((nb_alerts / step) - (not (nb_alerts % step) and 1 or 0)) # Check if the selected real page exceeds the maximum real page and reset # if needed if (real_page >= max_real_page): diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 90744d79e2..b13b319819 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5171,7 +5171,7 @@ def tmpl_yoursearches_display(self, <input name="p" value="%(p)s" type="text" /> <input class="formbutton" type="submit" value="%(submit_label)s" /> </form> - """ % {'search_text': _('Search inside your searches for'), + """ % {'search_text': _('Search inside all your searches for'), 'action': '%s/yoursearches/display?ln=%s' % (CFG_SITE_SECURE_URL, ln), 'p': cgi.escape(p), 'submit_label': _('Search')} diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index ab9a381277..a8b1c45fdc 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -84,60 +84,63 @@ def perform_request_yoursearches_display(uid, nb_queries_total = res_nb_queries[0][0] nb_queries_distinct = res_nb_queries[0][1] - # The real page starts counting from 0, i.e. minus 1 from the human page - real_page = page - 1 - # The step needs to be a positive integer - if (step <= 0): - step = CFG_WEBSEARCH_YOURSEARCHES_MAX_NUMBER_OF_DISPLAYED_SEARCHES - # The maximum real page is the integer division of the total number of - # searches and the searches displayed per page - max_real_page = (nb_queries_distinct / step) - (not (nb_queries_distinct % step) and 1 or 0) - # Check if the selected real page exceeds the maximum real page and reset - # if needed - if (real_page >= max_real_page): - #if ((nb_queries_distinct % step) != 0): - # real_page = max_real_page - #else: - # real_page = max_real_page - 1 - real_page = max_real_page - page = real_page + 1 - elif (real_page < 0): - real_page = 0 - page = 1 - # Calculate the start value for the SQL LIMIT constraint - limit_start = real_page * step - # Calculate the display of the paging navigation arrows for the template - paging_navigation = (real_page >= 2, - real_page >= 1, - real_page <= (max_real_page - 1), - (real_page <= (max_real_page - 2)) and (max_real_page + 1)) + if nb_queries_total: + # The real page starts counting from 0, i.e. minus 1 from the human page + real_page = page - 1 + # The step needs to be a positive integer + if (step <= 0): + step = CFG_WEBSEARCH_YOURSEARCHES_MAX_NUMBER_OF_DISPLAYED_SEARCHES + # The maximum real page is the integer division of the total number of + # searches and the searches displayed per page + max_real_page = nb_queries_distinct and ((nb_queries_distinct / step) - (not (nb_queries_distinct % step) and 1 or 0)) + # Check if the selected real page exceeds the maximum real page and reset + # if needed + if (real_page >= max_real_page): + #if ((nb_queries_distinct % step) != 0): + # real_page = max_real_page + #else: + # real_page = max_real_page - 1 + real_page = max_real_page + page = real_page + 1 + elif (real_page < 0): + real_page = 0 + page = 1 + # Calculate the start value for the SQL LIMIT constraint + limit_start = real_page * step + # Calculate the display of the paging navigation arrows for the template + paging_navigation = (real_page >= 2, + real_page >= 1, + real_page <= (max_real_page - 1), + (real_page <= (max_real_page - 2)) and (max_real_page + 1)) + + # Calculate the user search queries + query = """ SELECT DISTINCT(q.id), + q.urlargs, + DATE_FORMAT(MAX(uq.date),'%s') + FROM query q, + user_query uq + WHERE uq.id_user=%%s + AND uq.id_query=q.id + %s + GROUP BY uq.id_query + ORDER BY MAX(uq.date) DESC + LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', search_clause,) + params = (uid, limit_start, step) + result = run_sql(query, params) - - # Calculate the user search queries - query = """ SELECT DISTINCT(q.id), - q.urlargs, - DATE_FORMAT(MAX(uq.date),'%s') - FROM query q, - user_query uq - WHERE uq.id_user=%%s - AND uq.id_query=q.id - %s - GROUP BY uq.id_query - ORDER BY MAX(uq.date) DESC - LIMIT %%s,%%s""" % ('%%Y-%%m-%%d %%H:%%i:%%s', search_clause,) - params = (uid, limit_start, step) - result = run_sql(query, params) - - search_queries = [] - if result: - for search_query in result: - search_query_id = search_query[0] - search_query_args = search_query[1] - search_query_lastrun = search_query[2] or _("unknown") - search_queries.append({'id' : search_query_id, - 'args' : search_query_args, - 'lastrun' : search_query_lastrun, - 'user_alerts' : count_user_alerts_for_given_query(uid, search_query_id)}) + search_queries = [] + if result: + for search_query in result: + search_query_id = search_query[0] + search_query_args = search_query[1] + search_query_lastrun = search_query[2] or _("unknown") + search_queries.append({'id' : search_query_id, + 'args' : search_query_args, + 'lastrun' : search_query_lastrun, + 'user_alerts' : count_user_alerts_for_given_query(uid, search_query_id)}) + else: + search_queries = [] + paging_navigation = () return websearch_templates.tmpl_yoursearches_display( nb_queries_total = nb_queries_total, From bff80ccf6ba2933520bb90ded9bfe3e173d14552 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 2 Feb 2012 15:36:30 +0100 Subject: [PATCH 60/83] WebAlert & WebSearch: fix kwalitee reported issues * Fixes various warnings and errors reported by kwalitee. --- modules/webalert/lib/webalert.py | 10 +-- modules/webalert/lib/webalert_templates.py | 85 +++++++++---------- modules/webalert/lib/webalert_webinterface.py | 2 +- modules/websearch/lib/websearch_templates.py | 79 +++++++++-------- .../websearch/lib/websearch_webinterface.py | 4 +- .../websearch/lib/websearch_yoursearches.py | 5 +- 6 files changed, 83 insertions(+), 102 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index eda714e9b5..02f639acb1 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -31,7 +31,7 @@ from invenio.webbasket import create_personal_baskets_selection_box from invenio.webbasket_dblayer import check_user_owns_baskets from invenio.messages import gettext_set_language -from invenio.dateutils import convert_datestruct_to_datetext, convert_datetext_to_dategui +from invenio.dateutils import convert_datestruct_to_datetext import invenio.template webalert_templates = invenio.template.load('webalert') @@ -219,7 +219,6 @@ def perform_request_youralerts_display(uid, search_clause_urlargs = [] search_clause_alert_name = [] if p: - p_stripped = p.strip() p_stripped_args = p.split() sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg).replace('%','%%') + '%%\'' for p_stripped_arg in p_stripped_args] for sql_p_stripped_arg in sql_p_stripped_args: @@ -228,9 +227,6 @@ def perform_request_youralerts_display(uid, search_clause = "((%s) OR (%s))" % (" AND ".join(search_clause_urlargs), " AND ".join(search_clause_alert_name)) - idq_and_search_clause_list = [clause for clause in (idq_clause, search_clause) if clause] - idq_and_search_clause = ' AND '.join(idq_and_search_clause_list) - query_nb_alerts = """ SELECT COUNT(IF((uqb.id_user=%%s %s),uqb.id_query,NULL)), COUNT(q.id) @@ -343,9 +339,7 @@ def perform_request_youralerts_display(uid, page=page, step=step, paging_navigation=paging_navigation, - p=p, - guest=isGuestUser(uid), - guesttxt=warning_guest_user(type="alerts", ln=ln)) + p=p) return out def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG): diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index b08cc98b36..7f1e09a84b 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -270,9 +270,7 @@ def tmpl_youralerts_display(self, page, step, paging_navigation, - p, - guest, - guesttxt): + p): """ Displays an HTML formatted list of the user alerts. If the user has specified a query id, only the user alerts based on that @@ -297,13 +295,6 @@ def tmpl_youralerts_display(self, @param idq: The specified query id for which to display the user alerts @type idq: int - - @param guest: Whether the user is a guest or not - @type guest: boolean - - @param guesttxt: The HTML content of the warning box for guest users - (produced by webaccount.tmpl_warning_guest_user) - @type guesttxt: string """ # load the right message language @@ -794,51 +785,51 @@ def get_html_user_friendly_alert_query_args(args, _ = gettext_set_language(ln) # Arguments dictionary - dict = parse_qs(args) + args_dict = parse_qs(args) - if not dict.has_key('p') and not dict.has_key('p1') and not dict.has_key('p2') and not dict.has_key('p3'): + if not args_dict.has_key('p') and not args_dict.has_key('p1') and not args_dict.has_key('p2') and not args_dict.has_key('p3'): search_patterns_html = _('Searching for everything') else: search_patterns_html = _('Searching for') + ' ' - if dict.has_key('p'): - search_patterns_html += '<strong>' + cgi.escape(dict['p'][0]) + '</strong>' - if dict.has_key('f'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f'][0]) + '</strong>' - if dict.has_key('p1'): - if dict.has_key('p'): + if args_dict.has_key('p'): + search_patterns_html += '<strong>' + cgi.escape(args_dict['p'][0]) + '</strong>' + if args_dict.has_key('f'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f'][0]) + '</strong>' + if args_dict.has_key('p1'): + if args_dict.has_key('p'): search_patterns_html += ' ' + _('and') + ' ' - search_patterns_html += '<strong>' + cgi.escape(dict['p1'][0]) + '</strong>' - if dict.has_key('f1'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f1'][0]) + '</strong>' - if dict.has_key('p2'): - if dict.has_key('p') or dict.has_key('p1'): - if dict.has_key('op1'): - search_patterns_html += ' %s ' % (dict['op1'][0] == 'a' and _('and') or \ - dict['op1'][0] == 'o' and _('or') or \ - dict['op1'][0] == 'n' and _('and not') or + search_patterns_html += '<strong>' + cgi.escape(args_dict['p1'][0]) + '</strong>' + if args_dict.has_key('f1'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f1'][0]) + '</strong>' + if args_dict.has_key('p2'): + if args_dict.has_key('p') or args_dict.has_key('p1'): + if args_dict.has_key('op1'): + search_patterns_html += ' %s ' % (args_dict['op1'][0] == 'a' and _('and') or \ + args_dict['op1'][0] == 'o' and _('or') or \ + args_dict['op1'][0] == 'n' and _('and not') or ', ',) - search_patterns_html += '<strong>' + cgi.escape(dict['p2'][0]) + '</strong>' - if dict.has_key('f2'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f2'][0]) + '</strong>' - if dict.has_key('p3'): - if dict.has_key('p') or dict.has_key('p1') or dict.has_key('p2'): - if dict.has_key('op2'): - search_patterns_html += ' %s ' % (dict['op2'][0] == 'a' and _('and') or \ - dict['op2'][0] == 'o' and _('or') or \ - dict['op2'][0] == 'n' and _('and not') or + search_patterns_html += '<strong>' + cgi.escape(args_dict['p2'][0]) + '</strong>' + if args_dict.has_key('f2'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f2'][0]) + '</strong>' + if args_dict.has_key('p3'): + if args_dict.has_key('p') or args_dict.has_key('p1') or args_dict.has_key('p2'): + if args_dict.has_key('op2'): + search_patterns_html += ' %s ' % (args_dict['op2'][0] == 'a' and _('and') or \ + args_dict['op2'][0] == 'o' and _('or') or \ + args_dict['op2'][0] == 'n' and _('and not') or ', ',) - search_patterns_html += '<strong>' + cgi.escape(dict['p3'][0]) + '</strong>' - if dict.has_key('f3'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f3'][0]) + '</strong>' + search_patterns_html += '<strong>' + cgi.escape(args_dict['p3'][0]) + '</strong>' + if args_dict.has_key('f3'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f3'][0]) + '</strong>' - if not dict.has_key('c') and not dict.has_key('cc'): + if not args_dict.has_key('c') and not args_dict.has_key('cc'): collections_html = _('in all the collections') else: collections_html = _('in the following collection(s)') + ': ' - if dict.has_key('c'): - collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in dict['c']) - elif dict.has_key('cc'): - collections_html += '<strong>' + cgi.escape(dict['cc'][0]) + '</strong>' + if args_dict.has_key('c'): + collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in args_dict['c']) + elif args_dict.has_key('cc'): + collections_html += '<strong>' + cgi.escape(args_dict['cc'][0]) + '</strong>' search_query_args_html = search_patterns_html + '<br />' + collections_html @@ -884,9 +875,9 @@ def get_html_user_friendly_date_from_datetext(given_date, if given_date_datestruct[0] != 0 and \ given_date_datestruct[1] != 0 and \ given_date_datestruct[2] != 0: - days_old = (today - datetime_date(given_date_datestruct.tm_year, - given_date_datestruct.tm_mon, - given_date_datestruct.tm_mday)).days + days_old = (today - datetime_date(given_date_datestruct[0], + given_date_datestruct[1], + given_date_datestruct[2])).days if days_old == 0: out = _('Today') elif days_old < 7: diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index 6c2c850d99..17c23b014d 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -60,7 +60,7 @@ def index(self, req, dummy): redirect_to_url(req, '%s/youralerts/display' % CFG_SITE_SECURE_URL) - def list(self, req, form): + def list(self, req, dummy): """ Legacy youralerts list page. Now redirects to the youralerts display page. diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index b13b319819..5f95ee5674 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5092,8 +5092,6 @@ def tmpl_yoursearches_display(self, step, paging_navigation, p, - guest, - guesttxt, ln=CFG_SITE_LANG): """ Template for the display the user's search history. @@ -5256,8 +5254,7 @@ def tmpl_yoursearches_display(self, <tbody> %(yoursearches)s </tbody> -</table>""" % {'header': _('Search details'), - 'footer': footer, +</table>""" % {'footer': footer, 'yoursearches': yoursearches} return out @@ -5283,51 +5280,51 @@ def get_html_user_friendly_search_query_args(args, _ = gettext_set_language(ln) # Arguments dictionary - dict = parse_qs(args) + args_dict = parse_qs(args) - if not dict.has_key('p') and not dict.has_key('p1') and not dict.has_key('p2') and not dict.has_key('p3'): + if not args_dict.has_key('p') and not args_dict.has_key('p1') and not args_dict.has_key('p2') and not args_dict.has_key('p3'): search_patterns_html = _('Search for everything') else: search_patterns_html = _('Search for') + ' ' - if dict.has_key('p'): - search_patterns_html += '<strong>' + cgi.escape(dict['p'][0]) + '</strong>' - if dict.has_key('f'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f'][0]) + '</strong>' - if dict.has_key('p1'): - if dict.has_key('p'): + if args_dict.has_key('p'): + search_patterns_html += '<strong>' + cgi.escape(args_dict['p'][0]) + '</strong>' + if args_dict.has_key('f'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f'][0]) + '</strong>' + if args_dict.has_key('p1'): + if args_dict.has_key('p'): search_patterns_html += ' ' + _('and') + ' ' - search_patterns_html += '<strong>' + cgi.escape(dict['p1'][0]) + '</strong>' - if dict.has_key('f1'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f1'][0]) + '</strong>' - if dict.has_key('p2'): - if dict.has_key('p') or dict.has_key('p1'): - if dict.has_key('op1'): - search_patterns_html += ' %s ' % (dict['op1'][0] == 'a' and _('and') or \ - dict['op1'][0] == 'o' and _('or') or \ - dict['op1'][0] == 'n' and _('and not') or + search_patterns_html += '<strong>' + cgi.escape(args_dict['p1'][0]) + '</strong>' + if args_dict.has_key('f1'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f1'][0]) + '</strong>' + if args_dict.has_key('p2'): + if args_dict.has_key('p') or args_dict.has_key('p1'): + if args_dict.has_key('op1'): + search_patterns_html += ' %s ' % (args_dict['op1'][0] == 'a' and _('and') or \ + args_dict['op1'][0] == 'o' and _('or') or \ + args_dict['op1'][0] == 'n' and _('and not') or ', ',) - search_patterns_html += '<strong>' + cgi.escape(dict['p2'][0]) + '</strong>' - if dict.has_key('f2'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f2'][0]) + '</strong>' - if dict.has_key('p3'): - if dict.has_key('p') or dict.has_key('p1') or dict.has_key('p2'): - if dict.has_key('op2'): - search_patterns_html += ' %s ' % (dict['op2'][0] == 'a' and _('and') or \ - dict['op2'][0] == 'o' and _('or') or \ - dict['op2'][0] == 'n' and _('and not') or + search_patterns_html += '<strong>' + cgi.escape(args_dict['p2'][0]) + '</strong>' + if args_dict.has_key('f2'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f2'][0]) + '</strong>' + if args_dict.has_key('p3'): + if args_dict.has_key('p') or args_dict.has_key('p1') or args_dict.has_key('p2'): + if args_dict.has_key('op2'): + search_patterns_html += ' %s ' % (args_dict['op2'][0] == 'a' and _('and') or \ + args_dict['op2'][0] == 'o' and _('or') or \ + args_dict['op2'][0] == 'n' and _('and not') or ', ',) - search_patterns_html += '<strong>' + cgi.escape(dict['p3'][0]) + '</strong>' - if dict.has_key('f3'): - search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(dict['f3'][0]) + '</strong>' + search_patterns_html += '<strong>' + cgi.escape(args_dict['p3'][0]) + '</strong>' + if args_dict.has_key('f3'): + search_patterns_html += ' ' + _('as') + ' ' + '<strong>' + cgi.escape(args_dict['f3'][0]) + '</strong>' - if not dict.has_key('c') and not dict.has_key('cc'): + if not args_dict.has_key('c') and not args_dict.has_key('cc'): collections_html = _('in all the collections') else: collections_html = _('in the following collection(s)') + ': ' - if dict.has_key('c'): - collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in dict['c']) - elif dict.has_key('cc'): - collections_html += '<strong>' + cgi.escape(dict['cc'][0]) + '</strong>' + if args_dict.has_key('c'): + collections_html += ', '.join('<strong>' + cgi.escape(collection) + '</strong>' for collection in args_dict['c']) + elif args_dict.has_key('cc'): + collections_html += '<strong>' + cgi.escape(args_dict['cc'][0]) + '</strong>' search_query_args_html = search_patterns_html + '<br />' + collections_html @@ -5373,9 +5370,9 @@ def get_html_user_friendly_date_from_datetext(given_date, if given_date_datestruct[0] != 0 and \ given_date_datestruct[1] != 0 and \ given_date_datestruct[2] != 0: - days_old = (today - datetime_date(given_date_datestruct.tm_year, - given_date_datestruct.tm_mon, - given_date_datestruct.tm_mday)).days + days_old = (today - datetime_date(given_date_datestruct[0], + given_date_datestruct[1], + given_date_datestruct[2])).days if days_old == 0: out = _('Today') elif days_old < 7: diff --git a/modules/websearch/lib/websearch_webinterface.py b/modules/websearch/lib/websearch_webinterface.py index 9f779e46bc..6be0da4fcc 100644 --- a/modules/websearch/lib/websearch_webinterface.py +++ b/modules/websearch/lib/websearch_webinterface.py @@ -113,6 +113,7 @@ from invenio.bibfield import get_record from invenio.shellutils import mymkdir from invenio.websearch_yoursearches import perform_request_yoursearches_display +from invenio.webstat import register_customevent import invenio.template websearch_templates = invenio.template.load('websearch') @@ -1209,8 +1210,9 @@ class WebInterfaceYourSearchesPages(WebInterfaceDirectory): _exports = ['', 'display'] - def index(self, req, form): + def index(self, req, dummy): """ + Redirects the user to the display page. """ redirect_to_url(req, '%s/yoursearches/display' % CFG_SITE_SECURE_URL) diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index a8b1c45fdc..0d5461ef25 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -24,7 +24,7 @@ from invenio.webaccount import warning_guest_user from invenio.messages import gettext_set_language from invenio.webuser import isGuestUser -from urllib import quote, quote_plus, unquote_plus +from urllib import quote from invenio.webalert import count_user_alerts_for_given_query import invenio.template @@ -63,7 +63,6 @@ def perform_request_yoursearches_display(uid, search_clause = "" if p: - p_stripped = p.strip() p_stripped_args = p.split() sql_p_stripped_args = ['\'%%' + quote(p_stripped_arg).replace('%','%%') + '%%\'' for p_stripped_arg in p_stripped_args] for sql_p_stripped_arg in sql_p_stripped_args: @@ -150,8 +149,6 @@ def perform_request_yoursearches_display(uid, step=step, paging_navigation=paging_navigation, p=p, - guest = isGuestUser(uid), - guesttxt = warning_guest_user(type="searches", ln=ln), ln = ln) def account_list_searches(uid, From d2d82591fb6ce75c8676de470edcbbd5587222bd Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Wed, 24 Jul 2013 12:59:55 +0200 Subject: [PATCH 61/83] WebStyle: add new images to the Makefile * Adds new images for yoursearches and youralerts to the Makefile --- modules/webstyle/img/Makefile.am | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/webstyle/img/Makefile.am b/modules/webstyle/img/Makefile.am index 57bb29d3d1..169dcb0655 100644 --- a/modules/webstyle/img/Makefile.am +++ b/modules/webstyle/img/Makefile.am @@ -326,7 +326,13 @@ img_DATA = add-small.png \ yahoo_icon_24.png \ yahoo_icon_48.png \ yammer_icon_24.png \ - yammer_icon_48.png + yammer_icon_48.png \ + yoursearches_alert.png \ + yoursearches_alert_edit.png \ + yoursearches_search.png \ + youralerts_alert.png \ + youralerts_alert_delete.png \ + youralerts_alert_edit.png tmpdir=$(localstatedir)/tmp From 44999a9833f7350f4e75c59b1721834fd1eb97c0 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Fri, 8 Nov 2013 09:12:15 +0100 Subject: [PATCH 62/83] WebAlert: option to pause and resume alerts * Adds the option to pause and resume alerts. The option can be configured either in when editing a specific alert or when displaying all the alerts. (closes #1227) --- ...13_12_11_new_is_active_colum_for_alerts.py | 34 +++ modules/miscutil/sql/tabcreate.sql | 1 + modules/webalert/lib/alert_engine.py | 26 ++- modules/webalert/lib/webalert.py | 154 +++++++++++-- modules/webalert/lib/webalert_templates.py | 113 +++++++--- modules/webalert/lib/webalert_webinterface.py | 208 +++++++++++++++++- modules/webstyle/css/invenio.css | 28 ++- modules/webstyle/img/Makefile.am | 4 +- .../webstyle/img/youralerts_alert_pause.png | Bin 0 -> 1085 bytes .../webstyle/img/youralerts_alert_resume.png | Bin 0 -> 1011 bytes 10 files changed, 488 insertions(+), 80 deletions(-) create mode 100644 modules/miscutil/lib/upgrades/invenio_2013_12_11_new_is_active_colum_for_alerts.py create mode 100644 modules/webstyle/img/youralerts_alert_pause.png create mode 100644 modules/webstyle/img/youralerts_alert_resume.png diff --git a/modules/miscutil/lib/upgrades/invenio_2013_12_11_new_is_active_colum_for_alerts.py b/modules/miscutil/lib/upgrades/invenio_2013_12_11_new_is_active_colum_for_alerts.py new file mode 100644 index 0000000000..163daf8b79 --- /dev/null +++ b/modules/miscutil/lib/upgrades/invenio_2013_12_11_new_is_active_colum_for_alerts.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2013 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 is_active column for the user_query_basket table" + +def do_upgrade(): + run_sql(""" +ALTER TABLE user_query_basket ADD COLUMN is_active BOOL DEFAULT 1 AFTER notification; +""") + +def estimate(): + """ Estimate running time of upgrade in seconds (optional). """ + return 1 diff --git a/modules/miscutil/sql/tabcreate.sql b/modules/miscutil/sql/tabcreate.sql index 648f3fe4e2..583ab66838 100644 --- a/modules/miscutil/sql/tabcreate.sql +++ b/modules/miscutil/sql/tabcreate.sql @@ -3749,6 +3749,7 @@ CREATE TABLE IF NOT EXISTS user_query_basket ( alert_desc text default NULL, alert_recipient text default NULL, notification char(1) NOT NULL default 'y', + is_active tinyint(1) DEFAULT '1', PRIMARY KEY (id_user,id_query,frequency,id_basket), KEY alert_name (alert_name) ) ENGINE=MyISAM; diff --git a/modules/webalert/lib/alert_engine.py b/modules/webalert/lib/alert_engine.py index fb35bf5944..1e8f91b34b 100644 --- a/modules/webalert/lib/alert_engine.py +++ b/modules/webalert/lib/alert_engine.py @@ -63,15 +63,23 @@ def update_date_lastrun(alert): return run_sql('update user_query_basket set date_lastrun=%s where id_user=%s and id_query=%s and id_basket=%s;', (strftime("%Y-%m-%d"), alert[0], alert[1], alert[2],)) -def get_alert_queries(frequency): - """Return all the queries for the given frequency.""" - - return run_sql('select distinct id, urlargs from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.frequency=%s and uqb.date_lastrun <= now();', (frequency,)) - -def get_alert_queries_for_user(uid): - """Returns all the queries for the given user id.""" - - return run_sql('select distinct id, urlargs, uqb.frequency from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.id_user=%s and uqb.date_lastrun <= now();', (uid,)) +def get_alert_queries(frequency, only_active=True): + """ + Return all the active alert queries for the given frequency. + If only_active is False: fetch all the alert queries + regardless of them being active or not. + """ + + return run_sql('select distinct id, urlargs from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.frequency=%%s and uqb.date_lastrun <= now()%s;' % (only_active and ' and is_active = 1' or '', ), (frequency, )) + +def get_alert_queries_for_user(uid, only_active=True): + """ + Returns all the active alert queries for the given user id. + If only_active is False: fetch all the alert queries + regardless of them being active or not. + """ + + return run_sql('select distinct id, urlargs, uqb.frequency from query q, user_query_basket uqb where q.id=uqb.id_query and uqb.id_user=%%s and uqb.date_lastrun <= now()%s;' % (only_active and ' and is_active = 1' or '', ), (uid, )) def get_alerts(query, frequency): """Returns a dictionary of all the records found for a specific query and frequency along with other informationm""" diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 02f639acb1..5c262fee45 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -88,7 +88,16 @@ def check_user_can_add_alert(id_user, id_query): return True return False -def perform_input_alert(action, id_query, alert_name, frequency, notification, id_basket, uid, old_id_basket=None, ln = CFG_SITE_LANG): +def perform_input_alert(action, + id_query, + alert_name, + frequency, + notification, + id_basket, + uid, + is_active, + old_id_basket=None, + ln = CFG_SITE_LANG): """get the alert settings input: action="add" for a new alert (blank form), action="modify" for an update (get old values) @@ -96,21 +105,29 @@ def perform_input_alert(action, id_query, alert_name, frequency, notification, i for the "modify" action specify old alert_name, frequency of checking, e-mail notification and basket id. output: alert settings input form""" + # load the right language _ = gettext_set_language(ln) + # security check: if not check_user_can_add_alert(uid, id_query): raise AlertError(_("You do not have rights for this operation.")) + + # normalize is_active (it should be either 1 (True) or 0 (False)) + is_active = is_active and 1 or 0 + # display query information res = run_sql("SELECT urlargs FROM query WHERE id=%s", (id_query,)) try: urlargs = res[0][0] except: urlargs = "UNKNOWN" + baskets = create_personal_baskets_selection_box(uid=uid, html_select_box_name='idb', selected_bskid=old_id_basket, ln=ln) + return webalert_templates.tmpl_input_alert( ln = ln, query = get_textual_query_info_from_urlargs(urlargs, ln = ln), @@ -122,6 +139,7 @@ def perform_input_alert(action, id_query, alert_name, frequency, notification, i old_id_basket = old_id_basket, id_basket = id_basket, id_query = id_query, + is_active = is_active, guest = isGuestUser(uid), guesttxt = warning_guest_user(type="alerts", ln=ln) ) @@ -279,7 +297,8 @@ def perform_request_youralerts_display(uid, uqb.frequency, uqb.notification, DATE_FORMAT(uqb.date_creation,'%s'), - DATE_FORMAT(uqb.date_lastrun,'%s') + DATE_FORMAT(uqb.date_lastrun,'%s'), + uqb.is_active FROM user_query_basket uqb LEFT JOIN query q ON uqb.id_query=q.id @@ -305,7 +324,8 @@ def perform_request_youralerts_display(uid, alert_frequency, alert_notification, alert_creation, - alert_last_run) in result: + alert_last_run, + alert_is_active) in result: try: if not query_id: raise StandardError("""\ @@ -314,17 +334,18 @@ def perform_request_youralerts_display(uid, Please check this and delete it if needed. Otherwise no problem, I'm continuing with the other alerts now. Here are all the alerts defined by this user: %s""" % (uid, repr(result))) - alerts.append({'queryid' : query_id, - 'queryargs' : query_args, - 'textargs' : get_textual_query_info_from_urlargs(query_args, ln=ln), - 'userid' : uid, - 'basketid' : bsk_id, - 'basketname' : bsk_name, - 'alertname' : alert_name, - 'frequency' : alert_frequency, + alerts.append({'queryid' : query_id, + 'queryargs' : query_args, + 'textargs' : get_textual_query_info_from_urlargs(query_args, ln=ln), + 'userid' : uid, + 'basketid' : bsk_id, + 'basketname' : bsk_name, + 'alertname' : alert_name, + 'frequency' : alert_frequency, 'notification' : alert_notification, - 'created' : alert_creation, - 'lastrun' : alert_last_run}) + 'created' : alert_creation, + 'lastrun' : alert_last_run, + 'is_active' : alert_is_active}) except StandardError: register_exception(alert_admin=True) else: @@ -370,8 +391,91 @@ def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG) out += perform_request_youralerts_display(uid, idq=0, ln=ln) return out +def perform_pause_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG): + """Pause an alert + input: alert name + identifier of the query; + identifier of the basket + uid + output: confirmation message + the list of alerts Web page""" + + # load the right language + _ = gettext_set_language(ln) + + # security check: + if not check_user_can_add_alert(uid, id_query): + raise AlertError(_("You do not have rights for this operation.")) + + # set variables + out = "" + if (None in (alert_name, id_query, id_basket, uid)): + return out + + # DB call to pause the alert + query = """ UPDATE user_query_basket + SET is_active = 0 + WHERE id_user=%s + AND id_query=%s + AND id_basket=%s""" + params = (uid, id_query, id_basket) + res = run_sql(query, params) + + if res: + out += '<p class="info">%s</p>' % _('Alert successfully paused.') + else: + out += '<p class="warning">%s</p>' % _('Unable to pause alert.') + + out += perform_request_youralerts_display(uid, idq=0, ln=ln) + + return out + +def perform_resume_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG): + """Resume an alert + input: alert name + identifier of the query; + identifier of the basket + uid + output: confirmation message + the list of alerts Web page""" -def perform_update_alert(alert_name, frequency, notification, id_basket, id_query, old_id_basket, uid, ln = CFG_SITE_LANG): + # load the right language + _ = gettext_set_language(ln) + + # security check: + if not check_user_can_add_alert(uid, id_query): + raise AlertError(_("You do not have rights for this operation.")) + + # set variables + out = "" + if (None in (alert_name, id_query, id_basket, uid)): + return out + + # DB call to resume the alert + query = """ UPDATE user_query_basket + SET is_active = 1 + WHERE id_user=%s + AND id_query=%s + AND id_basket=%s""" + params = (uid, id_query, id_basket) + res = run_sql(query, params) + + if res: + out += '<p class="info">%s</p>' % _('Alert successfully resumed.') + else: + out += '<p class="warning">%s</p>' % _('Unable to resume alert.') + + out += perform_request_youralerts_display(uid, idq=0, ln=ln) + + return out + +def perform_update_alert(alert_name, + frequency, + notification, + id_basket, + id_query, + old_id_basket, + uid, + is_active, + ln = CFG_SITE_LANG): """update alert settings into the database input: the name of the new alert; alert frequency: 'month', 'week' or 'day'; @@ -383,9 +487,12 @@ def perform_update_alert(alert_name, frequency, notification, id_basket, id_quer output: confirmation message + the list of alerts Web page""" out = '' # sanity check - if (None in (alert_name, frequency, notification, id_basket, id_query, old_id_basket, uid)): + if (None in (alert_name, frequency, notification, id_basket, id_query, old_id_basket, uid, is_active)): return out + # normalize is_active (it should be either 1 (True) or 0 (False)) + is_active = is_active and 1 or 0 + # load the right language _ = gettext_set_language(ln) @@ -416,13 +523,20 @@ def perform_update_alert(alert_name, frequency, notification, id_basket, id_quer check_alert_is_unique( id_basket, id_query, uid, ln) # update a row into the alerts table: user_query_basket - query = """UPDATE user_query_basket - SET alert_name=%s,frequency=%s,notification=%s, - date_creation=%s,date_lastrun='',id_basket=%s - WHERE id_user=%s AND id_query=%s AND id_basket=%s""" + query = """ UPDATE user_query_basket + SET alert_name=%s, + frequency=%s, + notification=%s, + date_creation=%s, + date_lastrun='', + id_basket=%s, + is_active=%s + WHERE id_user=%s + AND id_query=%s + AND id_basket=%s""" params = (alert_name, frequency, notification, convert_datestruct_to_datetext(time.localtime()), - id_basket, uid, id_query, old_id_basket) + id_basket, is_active, uid, id_query, old_id_basket) run_sql(query, params) diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 7f1e09a84b..e8863d26fc 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -128,9 +128,20 @@ def tmpl_account_list_alerts(self, ln, alerts): } return out - def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notification, - baskets, old_id_basket, id_basket, id_query, - guest, guesttxt): + def tmpl_input_alert(self, + ln, + query, + alert_name, + action, + frequency, + notification, + baskets, + old_id_basket, + id_basket, + id_query, + is_active, + guest, + guesttxt): """ Displays an alert adding form. @@ -156,6 +167,8 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio - 'id_query' *string* - The id of the query associated to this alert + - 'is_active' *boolean* - is the alert active or not + - 'guest' *bool* - If the user is a guest user - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) @@ -197,6 +210,10 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio </select> </td> </tr> + <tr> + <td style="text-align: right; vertical-align:top; font-weight: bold;">%(is_active_label)s</td> + <td><input type="checkbox" name="is_active" value="1" %(is_active_checkbox)s/></td> + </tr> <tr> <td style="text-align:right; font-weight: bold">%(send_email)s</td> <td> @@ -209,7 +226,8 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio </tr> <tr> <td style="text-align: right; vertical-align:top; font-weight: bold;">%(store_basket)s</td> - <td>%(baskets)s + <td>%(baskets)s</td> + </tr> """ % { 'action': action, 'alert_name' : _("Alert identification name:"), @@ -229,29 +247,27 @@ def tmpl_input_alert(self, ln, query, alert_name, action, frequency, notificatio 'specify' : _("if %(x_fmt_open)sno%(x_fmt_close)s you must specify a basket") % {'x_fmt_open': '<b>', 'x_fmt_close': '</b>'}, 'store_basket' : _("Store results in basket?"), - 'baskets': baskets + 'baskets': baskets, + 'is_active_label' : _("Is the alert active?"), + 'is_active_checkbox' : is_active and 'checked="checked" ' or '', } - out += """ </td> - </tr> - <tr> + out += """<tr> <td colspan="2" style="text-align:center"> <input type="hidden" name="idq" value="%(idq)s" /> <input type="hidden" name="ln" value="%(ln)s" /> <input class="formbutton" type="submit" name="action" value="&nbsp;%(set_alert)s&nbsp;" />&nbsp; <input class="formbutton" type="reset" value="%(clear_data)s" /> - </td> - </tr> - </table> - </td> - </tr> + </td> + </tr> </table> - """ % { - 'idq' : id_query, - 'ln' : ln, - 'set_alert' : _("SET ALERT"), - 'clear_data' : _("CLEAR DATA"), - } + </td> + </tr> + </table>""" % {'idq' : id_query, + 'ln' : ln, + 'set_alert' : _("SET ALERT"), + 'clear_data' : _("CLEAR DATA"),} + if action == "update": out += '<input type="hidden" name="old_idb" value="%s" />' % old_id_basket out += "</form>" @@ -291,6 +307,7 @@ def tmpl_youralerts_display(self, 'notification' *string* - If notification should be sent by email ('y', 'n') 'created' *string* - The date of alert creation 'lastrun' *string* - The last running date + 'is_active' *boolean* - is the alert active or not @type alerts: list of dictionaries @param idq: The specified query id for which to display the user alerts @@ -379,7 +396,7 @@ def tmpl_youralerts_display(self, msg = _('You have defined a total of %(number_of_alerts)s alerts.') % \ {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alerts based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} @@ -417,6 +434,7 @@ def tmpl_youralerts_display(self, alert_notification = alert['notification'] alert_creation_date = alert['created'] alert_last_run_date = alert['lastrun'] + alert_active_p = alert['is_active'] alert_details_frequency = _('Runs') + '&nbsp;' + \ (alert_frequency == 'day' and '<strong>' + _('daily') + '</strong>' or \ @@ -444,16 +462,26 @@ def tmpl_youralerts_display(self, _('Last run:') + '&nbsp;' + \ alert_details_last_run_date + alert_details_options_pause_or_resume = create_html_link('%s/youralerts/%s' % \ + (CFG_SITE_SECURE_URL, alert_active_p and 'pause' or 'resume'), + {'ln' : ln, + 'idq' : alert_query_id, + 'name' : alert_name, + 'idb' : alert_basket_id,}, + alert_active_p and _('Pause') or _('Resume')) + alert_details_options_edit = create_html_link('%s/youralerts/modify' % \ (CFG_SITE_SECURE_URL,), - {'ln' : ln, - 'idq' : alert_query_id, - 'name' : alert_name, - 'freq' : alert_frequency, - 'notif' : alert_notification, - 'idb' : alert_basket_id, - 'old_idb': alert_basket_id}, + {'ln' : ln, + 'idq' : alert_query_id, + 'name' : alert_name, + 'freq' : alert_frequency, + 'notif' : alert_notification, + 'idb' : alert_basket_id, + 'is_active' : alert_active_p, + 'old_idb' : alert_basket_id}, _('Edit')) + alert_details_options_delete = create_html_link('%s/youralerts/remove' % \ (CFG_SITE_SECURE_URL,), {'ln' : ln, @@ -463,10 +491,18 @@ def tmpl_youralerts_display(self, _('Delete'), {'onclick': 'return confirm(\'%s\')' % \ (_('Are you sure you want to permanently delete this alert?'),)}) - alert_details_options = '<img src="%s/img/youralerts_alert_edit.png" />' % (CFG_SITE_URL,) + \ + + # TODO: find a nice way to format the display alert options + alert_details_options = '<img src="%s/img/youralerts_alert_%s.png" />' % \ + (CFG_SITE_URL, alert_active_p and 'pause' or 'resume') + \ + alert_details_options_pause_or_resume + \ + '&nbsp;&middot;&nbsp;' + \ + '<img src="%s/img/youralerts_alert_edit.png" />&nbsp;' % \ + (CFG_SITE_URL,) + \ alert_details_options_edit + \ - '&nbsp;&nbsp;&nbsp;' + \ - '<img src="%s/img/youralerts_alert_delete.png" />' % (CFG_SITE_URL,) + \ + '&nbsp;&middot;&nbsp;' + \ + '<img src="%s/img/youralerts_alert_delete.png" />' % \ + (CFG_SITE_URL,) + \ alert_details_options_delete youralerts_display_html += """ @@ -475,23 +511,28 @@ def tmpl_youralerts_display(self, %(counter)i. </td> <td class="youralerts_display_table_content" onMouseOver='this.className="youralerts_display_table_content_mouseover"' onMouseOut='this.className="youralerts_display_table_content"'> - <div class="youralerts_display_table_content_container_left"> - <div class="youralerts_display_table_content_name">%(alert_name)s</div> + <div class="youralerts_display_table_content_container_main%(css_class_content_is_active_p)s"> + <div class="youralerts_display_table_content_name">%(warning_label_is_active_p)s%(alert_name)s</div> <div class="youralerts_display_table_content_details">%(alert_details_frequency_notification_basket)s</div> <div class="youralerts_display_table_content_search_query">%(alert_details_search_query)s</div> </div> - <div class="youralerts_display_table_content_container_right"> + <div class="youralerts_display_table_content_clear"></div> + <div class="youralerts_display_table_content_container_left"> <div class="youralerts_display_table_content_options">%(alert_details_options)s</div> </div> - <div class="youralerts_display_table_content_clear"></div> - <div class="youralerts_display_table_content_dates">%(alert_details_creation_last_run_dates)s</div> + <div class="youralerts_display_table_content_container_right"> + <div class="youralerts_display_table_content_dates">%(alert_details_creation_last_run_dates)s</div> + </div> </td> </tr>""" % {'counter': counter, 'alert_name': cgi.escape(alert_name), 'alert_details_frequency_notification_basket': alert_details_frequency_notification_basket, 'alert_details_search_query': alert_details_search_query, 'alert_details_options': alert_details_options, - 'alert_details_creation_last_run_dates': alert_details_creation_last_run_dates} + 'alert_details_creation_last_run_dates': alert_details_creation_last_run_dates, + 'css_class_content_is_active_p' : not alert_active_p and ' youralerts_display_table_content_inactive' or '', + 'warning_label_is_active_p' : not alert_active_p and '<span class="warning">[&nbsp;%s&nbsp;]&nbsp;</span>' % _('paused') or '', + } footer = '' if paging_navigation[0]: diff --git a/modules/webalert/lib/webalert_webinterface.py b/modules/webalert/lib/webalert_webinterface.py index 17c23b014d..20edb2279f 100644 --- a/modules/webalert/lib/webalert_webinterface.py +++ b/modules/webalert/lib/webalert_webinterface.py @@ -22,13 +22,15 @@ __lastupdated__ = """$Date$""" from invenio.config import CFG_SITE_SECURE_URL, CFG_SITE_NAME, \ - CFG_ACCESS_CONTROL_LEVEL_SITE, CFG_SITE_NAME_INTL + CFG_ACCESS_CONTROL_LEVEL_SITE, CFG_SITE_NAME_INTL, CFG_SITE_LANG from invenio.webpage import page from invenio.webalert import perform_input_alert, \ perform_request_youralerts_display, \ perform_add_alert, \ perform_update_alert, \ perform_remove_alert, \ + perform_pause_alert, \ + perform_resume_alert, \ perform_request_youralerts_popular, \ AlertError from invenio.webuser import getUid, page_not_authorized, isGuestUser @@ -53,6 +55,8 @@ class WebInterfaceYourAlertsPages(WebInterfaceDirectory): 'add', 'update', 'remove', + 'pause', + 'resume', 'popular'] def index(self, req, dummy): @@ -101,8 +105,15 @@ def input(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = perform_input_alert("add", argd['idq'], argd['name'], argd['freq'], - argd['notif'], argd['idb'], uid, ln=argd['ln']) + html = perform_input_alert("add", + argd['idq'], + argd['name'], + argd['freq'], + argd['notif'], + argd['idb'], + uid, + is_active = 1, + ln = argd['ln']) except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), @@ -160,6 +171,7 @@ def modify(self, req, form): 'freq': (str, "week"), 'notif': (str, "y"), 'idb': (int, 0), + 'is_active': (int, 0), 'error_msg': (str, ""), }) @@ -186,8 +198,15 @@ def modify(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = perform_input_alert("update", argd['idq'], argd['name'], argd['freq'], - argd['notif'], argd['idb'], uid, argd['old_idb'], ln=argd['ln']) + html = perform_input_alert("update", argd['idq'], + argd['name'], + argd['freq'], + argd['notif'], + argd['idb'], + uid, + argd['is_active'], + argd['old_idb'], + ln=argd['ln']) except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), @@ -379,6 +398,7 @@ def update(self, req, form): 'notif': (str, None), 'idb': (int, None), 'idq': (int, None), + 'is_active': (int, 0), 'old_idb': (int, None), }) @@ -405,8 +425,15 @@ def update(self, req, form): text = _("You are not authorized to use alerts.")) try: - html = perform_update_alert(argd['name'], argd['freq'], argd['notif'], - argd['idb'], argd['idq'], argd['old_idb'], uid, ln=argd['ln']) + html = perform_update_alert(argd['name'], + argd['freq'], + argd['notif'], + argd['idb'], + argd['idq'], + argd['old_idb'], + uid, + argd['is_active'], + ln=argd['ln']) except AlertError, msg: return page(title=_("Error"), body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), @@ -450,6 +477,9 @@ def update(self, req, form): navmenuid='youralerts') def remove(self, req, form): + """ + Remove an alert from the DB. + """ argd = wash_urlargd(form, {'name': (str, None), 'idq': (int, None), @@ -525,6 +555,170 @@ def remove(self, req, form): lastupdated=__lastupdated__, navmenuid='youralerts') + def pause(self, req, form): + """ + Pause an alert. + """ + + argd = wash_urlargd(form, {'name' : (str, None), + 'idq' : (int, None), + 'idb' : (int, None), + 'ln' : (str, CFG_SITE_LANG), + }) + + uid = getUid(req) + + if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "%s/youralerts/pause" % \ + (CFG_SITE_SECURE_URL,), + navmenuid="youralerts") + elif uid == -1 or isGuestUser(uid): + return redirect_to_url(req, "%s/youraccount/login%s" % ( + CFG_SITE_SECURE_URL, + make_canonical_urlargd({ + 'referer' : "%s/youralerts/pause%s" % ( + CFG_SITE_SECURE_URL, + make_canonical_urlargd(argd, {})), + "ln" : argd['ln']}, {}))) + + # load the right language + _ = gettext_set_language(argd['ln']) + user_info = collect_user_info(req) + if not user_info['precached_usealerts']: + return page_not_authorized(req, "../", \ + text = _("You are not authorized to use alerts.")) + + try: + html = perform_pause_alert(argd['name'], + argd['idq'], + argd['idb'], + uid, + ln=argd['ln']) + except AlertError, msg: + return page(title=_("Error"), + body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Set a new alert") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='youralerts') + + # register event in webstat + alert_str = "%s (%d)" % (argd['name'], argd['idq']) + if user_info['email']: + user_str = "%s (%d)" % (user_info['email'], user_info['uid']) + else: + user_str = "" + try: + register_customevent("alerts", ["pause", alert_str, user_str]) + except: + register_exception(suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") + + # display success + return page(title=_("Display alerts"), + body=html, + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Display alerts") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='youralerts') + + def resume(self, req, form): + """ + Resume an alert. + """ + + argd = wash_urlargd(form, {'name' : (str, None), + 'idq' : (int, None), + 'idb' : (int, None), + 'ln' : (str, CFG_SITE_LANG), + }) + + uid = getUid(req) + + if CFG_ACCESS_CONTROL_LEVEL_SITE >= 1: + return page_not_authorized(req, "%s/youralerts/resume" % \ + (CFG_SITE_SECURE_URL,), + navmenuid="youralerts") + elif uid == -1 or isGuestUser(uid): + return redirect_to_url(req, "%s/youraccount/login%s" % ( + CFG_SITE_SECURE_URL, + make_canonical_urlargd({ + 'referer' : "%s/youralerts/resume%s" % ( + CFG_SITE_SECURE_URL, + make_canonical_urlargd(argd, {})), + "ln" : argd['ln']}, {}))) + + # load the right language + _ = gettext_set_language(argd['ln']) + user_info = collect_user_info(req) + if not user_info['precached_usealerts']: + return page_not_authorized(req, "../", \ + text = _("You are not authorized to use alerts.")) + + try: + html = perform_resume_alert(argd['name'], + argd['idq'], + argd['idb'], + uid, + ln=argd['ln']) + except AlertError, msg: + return page(title=_("Error"), + body=webalert_templates.tmpl_errorMsg(ln=argd['ln'], error_msg=msg), + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Set a new alert") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='youralerts') + + # register event in webstat + alert_str = "%s (%d)" % (argd['name'], argd['idq']) + if user_info['email']: + user_str = "%s (%d)" % (user_info['email'], user_info['uid']) + else: + user_str = "" + try: + register_customevent("alerts", ["resume", alert_str, user_str]) + except: + register_exception(suffix="Do the webstat tables exists? Try with 'webstatadmin --load-config'") + + # display success + return page(title=_("Display alerts"), + body=html, + navtrail= """<a class="navtrail" href="%(sitesecureurl)s/youraccount/display?ln=%(ln)s">%(account)s</a>""" % { + 'sitesecureurl' : CFG_SITE_SECURE_URL, + 'ln': argd['ln'], + 'account' : _("Your Account"), + }, + description=_("%s Personalize, Display alerts") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(argd['ln'], CFG_SITE_NAME), + uid=uid, + language=argd['ln'], + req=req, + lastupdated=__lastupdated__, + navmenuid='youralerts') + def popular(self, req, form): """ Display a list of popular alerts. diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index 2fdbeeb1f5..615d76e07f 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2583,6 +2583,7 @@ a:hover.bibMergeImgClickable img { .youralerts_display_table { border: 1px solid #ffcc00; + min-width: 800px; } .youralerts_display_table_header td { background-color: #ffffcc; @@ -2628,13 +2629,18 @@ a:hover.bibMergeImgClickable img { vertical-align: top; border-bottom: 1px solid #ffcc00; } +.youralerts_display_table_content_container_main { + width: 100%; + border-bottom: 1px solid #CCCCCC; + padding-bottom: 10px; +} .youralerts_display_table_content_container_left { float: left; - width: 75%; + width: 50%; } .youralerts_display_table_content_container_right { float: right; - width: 25%; + width: 50%; } .youralerts_display_table_content_clear { clear: both; @@ -2673,14 +2679,18 @@ a:hover.bibMergeImgClickable img { text-align: right; white-space: nowrap; color: gray; + margin-top: 5px; + /*margin-bottom: 5px;*/ } .youralerts_display_table_content_options { position: relative; - top: 0px; - right: 0px; - margin-bottom: 5px; - font-size: 80%; - text-align: right; + /*top: 0px; + right: 0px;*/ + margin-top: 5px; + /*margin-bottom: 5px;*/ + font-size: 70%; + text-align: left; + white-space: nowrap; } .youralerts_display_table_content_options img{ vertical-align: top; @@ -2694,6 +2704,10 @@ a:hover.bibMergeImgClickable img { text-decoration: underline; color: #000; } + +.youralerts_display_table_content_inactive { + color: #CCCCCC; +} /* end of WebAlert module */ /* WebSearch module - YourSearches */ diff --git a/modules/webstyle/img/Makefile.am b/modules/webstyle/img/Makefile.am index 169dcb0655..08119f8be2 100644 --- a/modules/webstyle/img/Makefile.am +++ b/modules/webstyle/img/Makefile.am @@ -332,7 +332,9 @@ img_DATA = add-small.png \ yoursearches_search.png \ youralerts_alert.png \ youralerts_alert_delete.png \ - youralerts_alert_edit.png + youralerts_alert_edit.png \ + youralerts_alert_pause.png \ + youralerts_alert_resume.png tmpdir=$(localstatedir)/tmp diff --git a/modules/webstyle/img/youralerts_alert_pause.png b/modules/webstyle/img/youralerts_alert_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..34c88d27d2a066b53a474d85e92b9352408fb389 GIT binary patch literal 1085 zcmV-D1j74?P)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z-3tf?ClO@mQi=co1G-5>K~y-)m6Fd-V^ti*&%N*VwX~pZUvL5}SYbj)1YI}*nGi=p zOavtkBoM-e9qK<|bmMMJCK+`hK_g=sp|uoQ5Ec^DL{t_mF$0k>Fr~vNG=(zIzW3Vq zj*Fql1c>@s-tX^ot{5F1{V5y{w*r6+g+i@8Jw2-6*h)-IO%(tjlgVr%o6YizFGu<C z(Kbaq9xwFv_NukDH6*^9f@PYht9u^GG|kOeELH~qet#LRUVRxaHoXKfF)>llbsZ&a z+s44afCnVGw6sK$jEsNHVzJ0*Y?9y^b{?fjlBSuXh(@C<wRx@mI(}IA5pTZL3B=>^ zLOPx1?CdNrVs>_xdwY8v8yj;0KmmYZ80hTmglU@4bsckabJ%{EL|4}v$eB49hJowt zZNjqilqYHc0DL|lq?G9D>Ov?O0tx6Ig`J%p00Opc<5E8P?AFa({+Pcm{Y(JJn>iMW z7RAWONU^fAk|ZxIlu}}McUKM%4{HDblANBNW;&fdcW@??VIq;>Q{5cLAxYY{%}6A2 znmieu;P~ALEz9BuAHL7=yAgK0-v0S%0ZFFQX{J&srcx=IR-Rki8?<eklhFw#Q=4pi z_1bM!nv}~uk*|bh=b@y6rfG0o2b!j!>$)pX)`H*ff9hDrB`n*5<2VSE`C;2Ou3UMJ zDv?OQb=`C7+_>>J%6w&*o}Nb72*O|HM|DlLd#Mj3`S|FN0Px$=B9D(B^Y*8=J{GZ9 ztXNZ1qphy4qNAe&wY9YuPP_~$CAPM<u&}UzrluyWtgK*YXh@XeN=nI=mKL<OwgLc> z$t33I=h4v6fad1ra}D?R_mR)%vAn#D_4ReMx3{Cdz8-^vgUDvHSX^8rfGP+8go$`O zUN~#rS#r4?CnqNvkH?wGWH>W3!*}0%hn8(}E4jh@57s%An4)D_bR37VZ=<~bV4YjZ z4cfNJYi+OmWAj-o%fivo5e&mXe}Dh!dv|v?{@VQ$j_X274T1uWQ-G8bQc4tyMQBn% z2*9NakH<rGVh{k};NSq7rlF#u0$#88Le0HiA2cc8)xB`(LP&|j!z{vK14?Nq6l^$d z5te14qN2i8uIq|yHj7XwgkUiEuckg5d<L!yIIe?Yv4B7zfN=Q92M?5&!|xA(E}>}( z)iu?WeSLk(FbpB36#s>L_kO>rNePeAp_B&!e;G=#@<0Gy-3O%<0HV68iVFXe7aE(s zzcc<-P6z=Z1ayxsNlE~Wd~@fc^^HHAQf^~Y<1c>$uo6%`whv3o00000NkvXXu0mjf Do{s3_ literal 0 HcmV?d00001 diff --git a/modules/webstyle/img/youralerts_alert_resume.png b/modules/webstyle/img/youralerts_alert_resume.png new file mode 100644 index 0000000000000000000000000000000000000000..7734298e153cc5c2b7ab307faf9f90ec893946dd GIT binary patch literal 1011 zcmV<P0}T9$P)<h;3K|Lk000e1NJLTq000;O000mO1^@s7dn1nn00004XF*Lt006O% z3;baP00006VoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru z-3tf?C^I$?51{}618_-1K~y-)os(T?TV)u>|L5dQQj=y)n^n+4lTC+}g7ij9OK*mu z6<P(AU_x(3>6C(c>7^p-+{9f7cC}6mz33KBQ!Qju7pkrUElLKPxoD-CD}~VZ<Rr_< z$I00_$@_R=6PmRvvftIq!}I<8UtxNB`q#F$wgCWuBuOR_QNEy(Fh4(U0zfvKy$e-U ziHJxo<VcVt$qWPnY;A1~0Pt7hHcCZ}n6B%0!{P7=>@xDoFusd?Ul8Wz=1iN-R<pw} z42+JBT0um#va&)o^7%XgfIvi4^X9}+DCCh$uHR$faJZi4wbx%oJbn}Y7yWIJB*|11 zg`&|YJ%}vJ6pcnHoleut%nSzr5db_M4@O2tpzAs~=ZHq50Du<v5%l&y4<#c*$)r(V zPbQP0gD_2lilsbh<q}n^6*_bFlQ|rQoZhLN`|M0$uT7YmnyR$7x9=Uk0Z|ko%d$8z zF<}7!5D_gbERdoo4ei9^aZ(h8B9RC+WUE%IL_}m52F=dS)&t&o_ibAF<9E7s`&L5$ zt39k%D~5-MkxHci06wn|$By~nbUJY@_9JX|_LOnIa;F}kX&O8pPrY(;vlEXsIY1nE zp|=<7$@S!o>o=~5yKTYYa5N0-a5w>QFbhMItqEJHZRCr^_gN$o0q1<*c6Q_)R5`)I zIWAqkgf~yWB^-K#PtSh*)zZ(g--yWGaEEX>T<PfOu&l1GVsLN}Ua$ASiVs5+MQm(r zU}<RyJv}{0BoY`K8>@H2L{X&v{(cM${P*)VH#ZTB#n9c|jlRCV1CyRV|2d96eiV0- z{~#0$p{uJ4<KyE1fJ7qEpcwXGaA#*{-ML&YV{vg2j4}BAek?C9qfjUS0uTxYQPN5< z?i={&+7DpNiYK4w*gw4_NoGw<HFRCyr^LNnxG+hkX^@hYDRVDPnQWSJ`7Ei09K~<` zN%Ilumtg3P(*T;;uDk&Nxm*qwiv=#13wFExp?kO4Z6FW?h!6z<5Cjk>aH_!T^Wpf@ z$4?Fp4V`>xXy_c{oC~U|qP4XZZnyh?9rlRJ1wjyCwc22142&@_X2tgQ7Oq~s0!`Om ze&)op*H4`qE8<c3@S_i=w6aD!@)jkNf2-eo`}O6E7pI>iqJ3Hx9;LvSlV7~o;%;#> hThnAH5KJBB`!9fbDi1czEiV87002ovPDHLkV1i#zz4QP8 literal 0 HcmV?d00001 From f23a368b44959b387fd4c0c7892638fa9ce8c5d6 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Mon, 17 Mar 2014 23:17:13 +0100 Subject: [PATCH 63/83] Webalert & Websearch: prettify display list header * Prettifies the display list header for Your Alerts and Your Searches by adding the paging navigation information. --- modules/webalert/lib/webalert_templates.py | 22 ++++++++++---------- modules/websearch/lib/websearch_templates.py | 22 ++++++++++---------- modules/webstyle/css/invenio.css | 18 +++++++++------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index e8863d26fc..46511e161e 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -534,42 +534,42 @@ def tmpl_youralerts_display(self, 'warning_label_is_active_p' : not alert_active_p and '<span class="warning">[&nbsp;%s&nbsp;]&nbsp;</span>' % _('paused') or '', } - footer = '' + paging_navigation_html = '' if paging_navigation[0]: - footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, 1, step, idq, ln, '/img/sb.gif') if paging_navigation[1]: - footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, page - 1, step, idq, ln, '/img/sp.gif') - footer += "&nbsp;" + paging_navigation_html += "&nbsp;" displayed_alerts_from = ((page - 1) * step) + 1 displayed_alerts_to = paging_navigation[2] and (page * step) or nb_alerts - footer += _('Displaying alerts <strong>%i to %i</strong> from <strong>%i</strong> total alerts') % \ + paging_navigation_html += _('Displaying alerts <strong>%i to %i</strong> from <strong>%i</strong> total alerts') % \ (displayed_alerts_from, displayed_alerts_to, nb_alerts) - footer += "&nbsp;" + paging_navigation_html += "&nbsp;" if paging_navigation[2]: - footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, page + 1, step, idq, ln, '/img/sn.gif') if paging_navigation[3]: - footer += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/youralerts/display?page=%i&amp;step=%i&amp;idq=%i&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, paging_navigation[3], step, idq, ln, '/img/se.gif') out += """ <table class="youralerts_display_table" cellspacing="0px"> <thead class="youralerts_display_table_header"> <tr> - <td colspan="2"></td> + <td colspan="2">%(paging_navigation_html)s</td> </tr> </thead> <tfoot class="youralerts_display_table_footer"> <tr> - <td colspan="2">%(footer)s</td> + <td colspan="2">%(paging_navigation_html)s</td> </tr> </tfoot> <tbody> %(youralerts_display_html)s </tbody> -</table>""" % {'footer': footer, +</table>""" % {'paging_navigation_html': paging_navigation_html, 'youralerts_display_html': youralerts_display_html} return out diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 5f95ee5674..7d7a44f898 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5219,42 +5219,42 @@ def tmpl_yoursearches_display(self, 'search_query_last_performed': search_query_last_performed, 'search_query_options': search_query_options} - footer = '' + paging_navigation_html = '' if paging_navigation[0]: - footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, 1, step, cgi.escape(p), ln, '/img/sb.gif') if paging_navigation[1]: - footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, page - 1, step, cgi.escape(p), ln, '/img/sp.gif') - footer += "&nbsp;" + paging_navigation_html += "&nbsp;" displayed_searches_from = ((page - 1) * step) + 1 displayed_searches_to = paging_navigation[2] and (page * step) or nb_queries_distinct - footer += _('Displaying searches <strong>%i to %i</strong> from <strong>%i</strong> total unique searches') % \ + paging_navigation_html += _('Displaying searches <strong>%i to %i</strong> from <strong>%i</strong> total unique searches') % \ (displayed_searches_from, displayed_searches_to, nb_queries_distinct) - footer += "&nbsp;" + paging_navigation_html += "&nbsp;" if paging_navigation[2]: - footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, page + 1, step, cgi.escape(p), ln, '/img/sn.gif') if paging_navigation[3]: - footer += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ + paging_navigation_html += """<a href="%s/yoursearches/display?page=%i&amp;step=%i&amp;p=%s&amp;ln=%s"><img src="%s" /></a>""" % \ (CFG_SITE_SECURE_URL, paging_navigation[3], step, cgi.escape(p), ln, '/img/se.gif') out += """ <table class="websearch_yoursearches_table" cellspacing="0px"> <thead class="websearch_yoursearches_table_header"> <tr> - <td colspan="2"></td> + <td colspan="2">%(paging_navigation_html)s</td> </tr> </thead> <tfoot class="websearch_yoursearches_table_footer"> <tr> - <td colspan="2">%(footer)s</td> + <td colspan="2">%(paging_navigation_html)s</td> </tr> </tfoot> <tbody> %(yoursearches)s </tbody> -</table>""" % {'footer': footer, +</table>""" % {'paging_navigation_html': paging_navigation_html, 'yoursearches': yoursearches} return out diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index 615d76e07f..d8dea42720 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2587,7 +2587,11 @@ a:hover.bibMergeImgClickable img { } .youralerts_display_table_header td { background-color: #ffffcc; + color: black; padding: 2px; + font-size: 80%; + text-align: center; + white-space: nowrap; border-bottom: 1px solid #ffcc00; } .youralerts_display_table_footer td { @@ -2598,7 +2602,7 @@ a:hover.bibMergeImgClickable img { text-align: center; white-space: nowrap; } -.youralerts_display_table_footer img { +.youralerts_display_table_footer img, .youralerts_display_table_header img { border: none; margin: 0px 3px; vertical-align: bottom; @@ -2717,13 +2721,11 @@ a:hover.bibMergeImgClickable img { .websearch_yoursearches_table_header td { background-color: #ffffcc; color: black; - padding: 2px; - font-weight: bold; - font-size: 75%; - text-transform: uppercase; - border-bottom: 1px solid #ffcc00; + padding: 5px; + font-size: 80%; + text-align: center; white-space: nowrap; - vertical-align: top; + border-bottom: 1px solid #ffcc00; } .websearch_yoursearches_table_footer td { background-color: #ffffcc; @@ -2733,7 +2735,7 @@ a:hover.bibMergeImgClickable img { text-align: center; white-space: nowrap; } -.websearch_yoursearches_table_footer img { +.websearch_yoursearches_table_footer img, .websearch_yoursearches_table_header img { border: none; margin: 0px 3px; vertical-align: bottom; From 57f93144e33dda1b8154ef2eae5aa30dbd69a66a Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Mon, 17 Mar 2014 23:51:26 +0100 Subject: [PATCH 64/83] Webalert: smart display of popular alerts link * Hides the link to the popular alerts if there are none already defined. --- modules/webalert/lib/webalert.py | 31 +++++++++++----- modules/webalert/lib/webalert_templates.py | 42 +++++++++++++++------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 5c262fee45..ae2db8db2f 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -352,15 +352,28 @@ def perform_request_youralerts_display(uid, alerts = [] paging_navigation = () - out = webalert_templates.tmpl_youralerts_display(ln=ln, - alerts=alerts, - nb_alerts=nb_alerts, - nb_queries=nb_queries, - idq=idq, - page=page, - step=step, - paging_navigation=paging_navigation, - p=p) + # check if there are any popular alerts already defined + query_popular_alerts_p = """ SELECT COUNT(q.id) + FROM query q + WHERE q.type='p'""" + result_popular_alerts_p = run_sql(query_popular_alerts_p) + if result_popular_alerts_p[0][0] > 0: + popular_alerts_p = True + else: + popular_alerts_p = False + + out = webalert_templates.tmpl_youralerts_display( + ln = ln, + alerts = alerts, + nb_alerts = nb_alerts, + nb_queries = nb_queries, + idq = idq, + page = page, + step = step, + paging_navigation = paging_navigation, + p = p, + popular_alerts_p = popular_alerts_p) + return out def perform_remove_alert(alert_name, id_query, id_basket, uid, ln=CFG_SITE_LANG): diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 46511e161e..5b6eb83121 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -286,7 +286,8 @@ def tmpl_youralerts_display(self, page, step, paging_navigation, - p): + p, + popular_alerts_p): """ Displays an HTML formatted list of the user alerts. If the user has specified a query id, only the user alerts based on that @@ -312,6 +313,21 @@ def tmpl_youralerts_display(self, @param idq: The specified query id for which to display the user alerts @type idq: int + + @param page: the page to be displayed + @type page: int + + @param step: the number of alerts to display per page + @type step: int + + @param paging_navigation: values to help display the paging navigation arrows + @type paging_navigation: tuple + + @param p: the search term (searching in alerts) + @type p: string + + @param popular_alerts_p: are there any popular alerts already defined? + @type popular_alerts_p: boolean """ # load the right message language @@ -330,17 +346,17 @@ def tmpl_youralerts_display(self, else: msg = _('The selected search query seems to be invalid.') msg += "<br />" - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alert based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} elif p and not idq: msg = _('You have not defined any alerts yet including the terms %s.') % \ ('<strong>' + cgi.escape(p) + '</strong>',) msg += "<br />" - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alert based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} elif p and idq: if nb_queries: @@ -353,16 +369,16 @@ def tmpl_youralerts_display(self, else: msg = _('The selected search query seems to be invalid.') msg += "<br />" - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alert based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} else: msg = _('You have not defined any alerts yet.') msg += '<br />' - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alert based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} out = '<p>' + msg + '</p>' return out @@ -380,9 +396,9 @@ def tmpl_youralerts_display(self, {'p': '<strong>' + cgi.escape(p) + '</strong>', 'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' - msg += _('You may define new alert based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alert based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} elif idq and p: msg = _('You have defined %(number_of_alerts)s alerts based on that search query including the terms %(p)s.') % \ @@ -396,9 +412,9 @@ def tmpl_youralerts_display(self, msg = _('You have defined a total of %(number_of_alerts)s alerts.') % \ {'number_of_alerts': '<strong>' + str(nb_alerts) + '</strong>'} msg += '<br />' - msg += _('You may define new alerts based on %(yoursearches)s, the %(popular_alerts)s or just by %(search_interface)s.') % \ + msg += _('You may define new alerts based on %(yoursearches)s%(popular_alerts)s or just by %(search_interface)s.') % \ {'yoursearches': '<a href="%s/yoursearches/display?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('your searches')), - 'popular_alerts': '<a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('popular alerts')), + 'popular_alerts': popular_alerts_p and ', <a href="%s/youralerts/popular?ln=%s">%s</a>' % (CFG_SITE_SECURE_URL, ln, _('the popular alerts')) or '', 'search_interface': '<a href="%s/?ln=%s">%s</a>' %(CFG_SITE_URL, ln, _('searching for something new'))} out = '<p>' + msg + '</p>' From d7ab23a1d7a905972690a135569804bdbb8715b2 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 20 Mar 2014 01:57:25 +0100 Subject: [PATCH 65/83] Webalert: improve store results in basket menu * Improves the appearence of the "Store results in basket?" menu. * Links the basket name to the basket display when displaying alerts. --- modules/webalert/lib/webalert.py | 16 ++--- modules/webalert/lib/webalert_templates.py | 65 +++++++++++++++++++- modules/webbasket/lib/webbasket.py | 17 ----- modules/webbasket/lib/webbasket_templates.py | 16 ----- 4 files changed, 72 insertions(+), 42 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index ae2db8db2f..f3b94c38fb 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -28,8 +28,9 @@ from invenio.webuser import isGuestUser from invenio.errorlib import register_exception from invenio.webaccount import warning_guest_user -from invenio.webbasket import create_personal_baskets_selection_box -from invenio.webbasket_dblayer import check_user_owns_baskets +from invenio.webbasket_dblayer import \ + check_user_owns_baskets, \ + get_all_user_personal_basket_ids_by_topic from invenio.messages import gettext_set_language from invenio.dateutils import convert_datestruct_to_datetext @@ -96,7 +97,7 @@ def perform_input_alert(action, id_basket, uid, is_active, - old_id_basket=None, + old_id_basket = None, ln = CFG_SITE_LANG): """get the alert settings input: action="add" for a new alert (blank form), action="modify" for an update @@ -123,10 +124,11 @@ def perform_input_alert(action, except: urlargs = "UNKNOWN" - baskets = create_personal_baskets_selection_box(uid=uid, - html_select_box_name='idb', - selected_bskid=old_id_basket, - ln=ln) + baskets = webalert_templates.tmpl_personal_basket_select_element( + bskid = old_id_basket, + personal_baskets_list = get_all_user_personal_basket_ids_by_topic(uid), + select_element_name = "idb", + ln = ln) return webalert_templates.tmpl_input_alert( ln = ln, diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 5b6eb83121..afc50d8fd5 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -36,6 +36,8 @@ from invenio.urlutils import create_html_link from invenio.search_engine import guess_primary_collection_of_a_record, get_coll_ancestors from invenio.dateutils import convert_datetext_to_datestruct +from invenio.webbasket_dblayer import get_basket_ids_and_names +from invenio.webbasket_config import CFG_WEBBASKET_CATEGORIES class Template: def tmpl_errorMsg(self, ln, error_msg, rest = ""): @@ -458,8 +460,14 @@ def tmpl_youralerts_display(self, alert_frequency == 'month' and '<strong>' + _('monthly') + '</strong>') alert_details_notification = alert_notification == 'y' and _('You are notified by <strong>e-mail</strong>') or \ alert_notification == 'n' and '' - alert_details_basket = alert_basket_name and _('The results are automatically added to your basket:') + \ - '&nbsp;' + '<strong>' + cgi.escape(alert_basket_name) + '</strong>' or '' + alert_details_basket = alert_basket_name and '%s&nbsp;<strong><a href="%s/yourbaskets/display?category=%s&bskid=%s&ln=%s">%s</a></strong>' % ( + _('The results are automatically added to your personal basket:'), + CFG_SITE_SECURE_URL, + CFG_WEBBASKET_CATEGORIES['PRIVATE'], + str(alert_basket_id), + ln, + cgi.escape(alert_basket_name)) \ + or '' alert_details_frequency_notification_basket = alert_details_frequency + \ (alert_details_notification and \ '&nbsp;/&nbsp;' + \ @@ -821,6 +829,59 @@ def tmpl_youralerts_popular(self, return out + def tmpl_personal_basket_select_element( + self, + bskid, + personal_baskets_list, + select_element_name, + ln): + """ + Returns an HTML select element with the user's personal baskets as the list of options. + """ + + _ = gettext_set_language(ln) + + out = """ + <select name="%s">""" % (select_element_name,) + + # Calculate the selected basket if there is one pre-selected. + bskid = bskid and str(bskid) or "" + + # Create the default disabled label option. + out += """ + <option value="%(value)i"%(selected)s>%(label)s</option>""" % \ + {'value': 0, + 'selected': bskid == '' and ' selected="selected"' or '', + 'label': _("Don't store results in basket...")} + + # Create the <optgroup>s and <option>s for the user personal baskets. + if personal_baskets_list: + out += """ + <optgroup label="%s">""" % ('* ' + _('Your personal baskets') + ' *',) + for baskets_topic_and_bskids in personal_baskets_list: + topic = baskets_topic_and_bskids[0] + bskids = baskets_topic_and_bskids[1].split(',') + out += """ + <optgroup label="%s">""" % (cgi.escape(topic, True),) + bskids_and_names = get_basket_ids_and_names(bskids) + for bskid_and_name in bskids_and_names: + basket_value = str(bskid_and_name[0]) + basket_name = bskid_and_name[1] + out += """ + <option value="%(value)s"%(selected)s>%(label)s</option>""" % \ + {'value': basket_value, + 'selected': basket_value == bskid and ' selected="selected"' or '', + 'label': cgi.escape(basket_name, True)} + out += """ + </optgroup>""" + out += """ + </optgroup>""" + + out += """ + </select>""" + + return out + def get_html_user_friendly_alert_query_args(args, ln=CFG_SITE_LANG): """ diff --git a/modules/webbasket/lib/webbasket.py b/modules/webbasket/lib/webbasket.py index 0a193eabb2..73710571b5 100644 --- a/modules/webbasket/lib/webbasket.py +++ b/modules/webbasket/lib/webbasket.py @@ -2394,23 +2394,6 @@ def create_guest_warning_box(ln=CFG_SITE_LANG): """return a warning message about logging into system""" return webbasket_templates.tmpl_create_guest_warning_box(ln) -def create_personal_baskets_selection_box(uid, - html_select_box_name='baskets', - selected_bskid=None, - ln=CFG_SITE_LANG): - """Return HTML box for basket selection. Only for personal baskets. - @param uid: user id - @param html_select_box_name: name used in html form - @param selected_bskid: basket currently selected - @param ln: language - """ - baskets = db.get_all_personal_baskets_names(uid) - return webbasket_templates.tmpl_personal_baskets_selection_box( - baskets, - html_select_box_name, - selected_bskid, - ln) - def create_basket_navtrail(uid, category=CFG_WEBBASKET_CATEGORIES['PRIVATE'], topic="", group=0, diff --git a/modules/webbasket/lib/webbasket_templates.py b/modules/webbasket/lib/webbasket_templates.py index 096d9fffba..77d4c290a5 100644 --- a/modules/webbasket/lib/webbasket_templates.py +++ b/modules/webbasket/lib/webbasket_templates.py @@ -2116,22 +2116,6 @@ def tmpl_add_group(self, bskid, selected_topic, groups=[], ln=CFG_SITE_LANG): 'submit_label': _("Add group")} return out - def tmpl_personal_baskets_selection_box(self, - baskets=[], - select_box_name='baskets', - selected_bskid=None, - ln=CFG_SITE_LANG): - """return an HTML popupmenu - @param baskets: list of (bskid, bsk_name, bsk_topic) tuples - @param select_box_name: name that will be used for the control - @param selected_bskid: id of the selcte basket, use None for no selection - @param ln: language""" - _ = gettext_set_language(ln) - elements = [(0, '- ' + _("no basket") + ' -')] - for (bskid, bsk_name, bsk_topic) in baskets: - elements.append((bskid, bsk_topic + ' &gt; ' + bsk_name)) - return self.__create_select_menu(select_box_name, elements, selected_bskid) - def tmpl_create_guest_warning_box(self, ln=CFG_SITE_LANG): """return html warning box for non registered users""" _ = gettext_set_language(ln) From 70ca3f16b25126a0b0b16e35182a7c2bc1ae08b2 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 20 Mar 2014 16:32:05 +0100 Subject: [PATCH 66/83] Webalert & Websearch: harmonize display * Harmonizes and prettifies the display of "Your Searches" and "Your Alerts" --- modules/webalert/lib/webalert_templates.py | 60 ++++++++++---------- modules/websearch/lib/websearch_templates.py | 38 +++++++------ modules/webstyle/css/invenio.css | 42 +++++--------- 3 files changed, 68 insertions(+), 72 deletions(-) diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index afc50d8fd5..e4f87e0d72 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -461,7 +461,7 @@ def tmpl_youralerts_display(self, alert_details_notification = alert_notification == 'y' and _('You are notified by <strong>e-mail</strong>') or \ alert_notification == 'n' and '' alert_details_basket = alert_basket_name and '%s&nbsp;<strong><a href="%s/yourbaskets/display?category=%s&bskid=%s&ln=%s">%s</a></strong>' % ( - _('The results are automatically added to your personal basket:'), + _('The results are added to your basket:'), CFG_SITE_SECURE_URL, CFG_WEBBASKET_CATEGORIES['PRIVATE'], str(alert_basket_id), @@ -470,21 +470,21 @@ def tmpl_youralerts_display(self, or '' alert_details_frequency_notification_basket = alert_details_frequency + \ (alert_details_notification and \ - '&nbsp;/&nbsp;' + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ alert_details_notification) + \ (alert_details_basket and \ - '&nbsp;/&nbsp;' + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ alert_details_basket) alert_details_search_query = get_html_user_friendly_alert_query_args(alert_query_args, ln) alert_details_creation_date = get_html_user_friendly_date_from_datetext(alert_creation_date, True, False, ln) alert_details_last_run_date = get_html_user_friendly_date_from_datetext(alert_last_run_date, True, False, ln) - alert_details_creation_last_run_dates = _('Created:') + '&nbsp;' + \ - alert_details_creation_date + \ - '&nbsp;/&nbsp;' + \ - _('Last run:') + '&nbsp;' + \ - alert_details_last_run_date + alert_details_creation_last_run_dates = _('Last run:') + '&nbsp;' + \ + alert_details_last_run_date + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ + _('Created:') + '&nbsp;' + \ + alert_details_creation_date alert_details_options_pause_or_resume = create_html_link('%s/youralerts/%s' % \ (CFG_SITE_SECURE_URL, alert_active_p and 'pause' or 'resume'), @@ -520,11 +520,11 @@ def tmpl_youralerts_display(self, alert_details_options = '<img src="%s/img/youralerts_alert_%s.png" />' % \ (CFG_SITE_URL, alert_active_p and 'pause' or 'resume') + \ alert_details_options_pause_or_resume + \ - '&nbsp;&middot;&nbsp;' + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ '<img src="%s/img/youralerts_alert_edit.png" />&nbsp;' % \ (CFG_SITE_URL,) + \ alert_details_options_edit + \ - '&nbsp;&middot;&nbsp;' + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ '<img src="%s/img/youralerts_alert_delete.png" />' % \ (CFG_SITE_URL,) + \ alert_details_options_delete @@ -536,19 +536,15 @@ def tmpl_youralerts_display(self, </td> <td class="youralerts_display_table_content" onMouseOver='this.className="youralerts_display_table_content_mouseover"' onMouseOut='this.className="youralerts_display_table_content"'> <div class="youralerts_display_table_content_container_main%(css_class_content_is_active_p)s"> - <div class="youralerts_display_table_content_name">%(warning_label_is_active_p)s%(alert_name)s</div> - <div class="youralerts_display_table_content_details">%(alert_details_frequency_notification_basket)s</div> + <div class="youralerts_display_table_content_name"><strong>%(warning_label_is_active_p)s</strong>%(alert_name_label)s&nbsp;<strong>%(alert_name)s</strong></div> <div class="youralerts_display_table_content_search_query">%(alert_details_search_query)s</div> + <div class="youralerts_display_table_content_details">%(alert_details_frequency_notification_basket)s</div> </div> - <div class="youralerts_display_table_content_clear"></div> - <div class="youralerts_display_table_content_container_left"> - <div class="youralerts_display_table_content_options">%(alert_details_options)s</div> - </div> - <div class="youralerts_display_table_content_container_right"> - <div class="youralerts_display_table_content_dates">%(alert_details_creation_last_run_dates)s</div> - </div> + <div class="youralerts_display_table_content_dates">%(alert_details_creation_last_run_dates)s</div> + <div class="youralerts_display_table_content_options">%(alert_details_options)s</div> </td> </tr>""" % {'counter': counter, + 'alert_name_label' : _('Alert'), 'alert_name': cgi.escape(alert_name), 'alert_details_frequency_notification_basket': alert_details_frequency_notification_basket, 'alert_details_search_query': alert_details_search_query, @@ -999,7 +995,7 @@ def get_html_user_friendly_date_from_datetext(given_date, if days_old == 0: out = _('Today') elif days_old < 7: - out = str(days_old) + ' ' + _('day(s) ago') + out = str(days_old) + ' ' + _('days ago') elif days_old == 7: out = _('A week ago') elif days_old < 14: @@ -1010,21 +1006,27 @@ def get_html_user_friendly_date_from_datetext(given_date, out = _('More than two weeks ago') elif days_old == 30: out = _('A month ago') - elif days_old < 180: + elif days_old < 90: out = _('More than a month ago') + elif days_old < 180: + out = _('More than three months ago') elif days_old < 365: out = _('More than six months ago') - else: + elif days_old < 730: out = _('More than a year ago') + elif days_old < 1095: + out = _('More than two years ago') + elif days_old < 1460: + out = _('More than three years ago') + elif days_old < 1825: + out = _('More than four years ago') + else: + out = _('More than five years ago') if show_full_date: - out += '<span style="color: gray;">' + \ - '&nbsp;' + _('on') + '&nbsp;' + \ - given_date.split()[0] + '</span>' + out += '&nbsp;' + _('on') + '&nbsp;' + given_date.split()[0] if show_full_time: - out += '<span style="color: gray;">' + \ - '&nbsp;' + _('at') + '&nbsp;' + \ - given_date.split()[1] + '</span>' + out += '&nbsp;' + _('at') + '&nbsp;' + given_date.split()[1] else: - out = _('Unknown') + out = _('unknown') return out diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 7d7a44f898..e6bdbc13ec 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5193,14 +5193,14 @@ def tmpl_yoursearches_display(self, search_query_options_alert = search_query_number_of_user_alerts and \ """<a href="%s/youralerts/display?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert_edit.png" />%s</a>""" % \ - (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Edit your existing alert(s)')) + \ - '&nbsp;&nbsp;&nbsp;' + \ + (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Edit your existing alerts')) + \ + '&nbsp;<strong>&middot;</strong>&nbsp;' + \ """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up as a new alert')) or \ """<a href="%s/youralerts/input?ln=%s&amp;idq=%i"><img src="%s/img/yoursearches_alert.png" />%s</a>""" % \ (CFG_SITE_SECURE_URL, ln, search_query_id, CFG_SITE_URL, _('Set up as a new alert')) - search_query_options = "%s&nbsp;&nbsp;&nbsp;%s" % \ + search_query_options = "%s&nbsp;<strong>&middot;</strong>&nbsp;%s" % \ (search_query_options_search, \ search_query_options_alert) @@ -5211,7 +5211,7 @@ def tmpl_yoursearches_display(self, </td> <td class="websearch_yoursearches_table_content" onMouseOver='this.className="websearch_yoursearches_table_content_mouseover"' onMouseOut='this.className="websearch_yoursearches_table_content"'> <div>%(search_query_details)s</div> - <div class="websearch_yoursearches_table_content_options">%(search_query_last_performed)s</div> + <div class="websearch_yoursearches_table_content_dates">%(search_query_last_performed)s</div> <div class="websearch_yoursearches_table_content_options">%(search_query_options)s</div> </td> </tr>""" % {'counter': counter, @@ -5283,9 +5283,9 @@ def get_html_user_friendly_search_query_args(args, args_dict = parse_qs(args) if not args_dict.has_key('p') and not args_dict.has_key('p1') and not args_dict.has_key('p2') and not args_dict.has_key('p3'): - search_patterns_html = _('Search for everything') + search_patterns_html = _('You searched for everything') else: - search_patterns_html = _('Search for') + ' ' + search_patterns_html = _('You searched for') + ' ' if args_dict.has_key('p'): search_patterns_html += '<strong>' + cgi.escape(args_dict['p'][0]) + '</strong>' if args_dict.has_key('f'): @@ -5376,7 +5376,7 @@ def get_html_user_friendly_date_from_datetext(given_date, if days_old == 0: out = _('Today') elif days_old < 7: - out = str(days_old) + ' ' + _('day(s) ago') + out = str(days_old) + ' ' + _('days ago') elif days_old == 7: out = _('A week ago') elif days_old < 14: @@ -5387,21 +5387,27 @@ def get_html_user_friendly_date_from_datetext(given_date, out = _('More than two weeks ago') elif days_old == 30: out = _('A month ago') - elif days_old < 180: + elif days_old < 90: out = _('More than a month ago') + elif days_old < 180: + out = _('More than three months ago') elif days_old < 365: out = _('More than six months ago') - else: + elif days_old < 730: out = _('More than a year ago') + elif days_old < 1095: + out = _('More than two years ago') + elif days_old < 1460: + out = _('More than three years ago') + elif days_old < 1825: + out = _('More than four years ago') + else: + out = _('More than five years ago') if show_full_date: - out += '<span style="color: gray;">' + \ - '&nbsp;' + _('on') + '&nbsp;' + \ - given_date.split()[0] + '</span>' + out += '&nbsp;' + _('on') + '&nbsp;' + given_date.split()[0] if show_full_time: - out += '<span style="color: gray;">' + \ - '&nbsp;' + _('at') + '&nbsp;' + \ - given_date.split()[1] + '</span>' + out += '&nbsp;' + _('at') + '&nbsp;' + given_date.split()[1] else: - out = _('Unknown') + out = _('unknown') return out diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index d8dea42720..2849d124bb 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -2635,19 +2635,6 @@ a:hover.bibMergeImgClickable img { } .youralerts_display_table_content_container_main { width: 100%; - border-bottom: 1px solid #CCCCCC; - padding-bottom: 10px; -} -.youralerts_display_table_content_container_left { - float: left; - width: 50%; -} -.youralerts_display_table_content_container_right { - float: right; - width: 50%; -} -.youralerts_display_table_content_clear { - clear: both; } .youralerts_display_table_counter { background-color: white; @@ -2660,40 +2647,33 @@ a:hover.bibMergeImgClickable img { } .youralerts_display_table_content_name { margin-bottom: 5px; - font-weight: bold; position: relative; left: 0px; top: 0px; } .youralerts_display_table_content_details { margin-bottom: 5px; - font-size: 80%; position: relative; left: 0px; } .youralerts_display_table_content_search_query { margin-bottom: 5px; - font-size: 80%; position: relative; left: 0px; } .youralerts_display_table_content_dates { + margin-bottom: 5px; position: relative; - font-size: 70%; - text-align: right; - white-space: nowrap; + left: 0px; + font-size: 80%; color: gray; - margin-top: 5px; - /*margin-bottom: 5px;*/ + white-space: nowrap; } .youralerts_display_table_content_options { + margin-bottom: 5px; position: relative; - /*top: 0px; - right: 0px;*/ - margin-top: 5px; - /*margin-bottom: 5px;*/ - font-size: 70%; - text-align: left; + left: 0px; + font-size: 80%; white-space: nowrap; } .youralerts_display_table_content_options img{ @@ -2712,6 +2692,9 @@ a:hover.bibMergeImgClickable img { .youralerts_display_table_content_inactive { color: #CCCCCC; } +.youralerts_display_table_content_inactive a, .youralerts_display_table_content_inactive a:link, .youralerts_display_table_content_inactive a:visited, .youralerts_display_table_content_inactive a:active, .youralerts_display_table_content_inactive a:hover { + color: #CCCCCC; +} /* end of WebAlert module */ /* WebSearch module - YourSearches */ @@ -2775,6 +2758,11 @@ a:hover.bibMergeImgClickable img { color: grey; border-bottom: 1px solid #ffcc00; } +.websearch_yoursearches_table_content_dates { + color: gray; + margin: 5px; + font-size: 80%; +} .websearch_yoursearches_table_content_options { margin: 5px; font-size: 80%; From 7738dbbf2147eb3878be6a8abc8b1ae94888853c Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Mon, 24 Mar 2014 19:55:04 +0100 Subject: [PATCH 67/83] Websession: Revamp the "Your Account" page * Revamps the "Your Account" page by arranging items to be displayed in a 3-column grid, trying to keep all columns equal in size. * Re-arranges code for consistency and readability. --- modules/webalert/lib/webalert.py | 42 +-- modules/webalert/lib/webalert_templates.py | 52 +-- modules/webbasket/lib/webbasket.py | 31 +- modules/webbasket/lib/webbasket_dblayer.py | 49 ++- modules/webbasket/lib/webbasket_templates.py | 28 ++ modules/webcomment/lib/webcomment.py | 17 + .../webcomment/lib/webcomment_templates.py | 17 + modules/webmessage/lib/webmessage.py | 19 +- .../webmessage/lib/webmessage_templates.py | 33 +- modules/websearch/lib/websearch_templates.py | 24 +- .../websearch/lib/websearch_yoursearches.py | 35 +- modules/websession/lib/webaccount.py | 160 +++++---- modules/websession/lib/webgroup.py | 26 +- .../websession/lib/websession_templates.py | 307 ++++++++++++------ .../websession/lib/websession_webinterface.py | 80 ++--- modules/webstyle/css/invenio.css | 66 ++-- modules/websubmit/lib/websubmit_templates.py | 17 + 17 files changed, 606 insertions(+), 397 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index f3b94c38fb..477dc4d8ca 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -27,7 +27,6 @@ from invenio.dbquery import run_sql from invenio.webuser import isGuestUser from invenio.errorlib import register_exception -from invenio.webaccount import warning_guest_user from invenio.webbasket_dblayer import \ check_user_owns_baskets, \ get_all_user_personal_basket_ids_by_topic @@ -141,10 +140,7 @@ def perform_input_alert(action, old_id_basket = old_id_basket, id_basket = id_basket, id_query = id_query, - is_active = is_active, - guest = isGuestUser(uid), - guesttxt = warning_guest_user(type="alerts", ln=ln) - ) + is_active = is_active) def check_alert_is_unique(id_basket, id_query, uid, ln=CFG_SITE_LANG ): """check the user does not have another alert for the specified query and basket""" @@ -566,30 +562,22 @@ def is_selected(var, fld): else: return "" -def account_list_alerts(uid, ln=CFG_SITE_LANG): - """account_list_alerts: list alert for the account page +def account_user_alerts(uid, ln=CFG_SITE_LANG): + """ + Information on the user's alerts for the "Your Account" page. input: the user id language - output: the list of alerts Web page""" - query = """ SELECT q.id, q.urlargs, a.id_user, a.id_query, - a.id_basket, a.alert_name, a.frequency, - a.notification, - DATE_FORMAT(a.date_creation,'%%d %%b %%Y'), - DATE_FORMAT(a.date_lastrun,'%%d %%b %%Y'), - a.id_basket - FROM query q, user_query_basket a - WHERE a.id_user=%s AND a.id_query=q.id - ORDER BY a.alert_name ASC """ - res = run_sql(query, (uid,)) - alerts = [] - if len(res): - for row in res: - alerts.append({ - 'id' : row[0], - 'name' : row[5] - }) + output: the information in HTML + """ + + query = """ SELECT COUNT(*) + FROM user_query_basket + WHERE id_user = %s""" + params = (uid,) + result = run_sql(query, params) + alerts = result[0][0] - return webalert_templates.tmpl_account_list_alerts(ln=ln, alerts=alerts) + return webalert_templates.tmpl_account_user_alerts(alerts, ln) def perform_request_youralerts_popular(ln=CFG_SITE_LANG): """ @@ -636,7 +624,7 @@ def count_user_alerts_for_given_query(id_user, """ query = """ SELECT COUNT(id_query) - FROM user_query_basket AS uqb + FROM user_query_basket WHERE id_user=%s AND id_query=%s""" params = (id_user, id_query) diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index e4f87e0d72..34d8939465 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -99,35 +99,30 @@ def tmpl_textual_query_info_from_urlargs(self, ln, args): out += "<strong>" + _("Collection") + ":</strong> " + "; ".join(args['cc']) + "<br />" return out - def tmpl_account_list_alerts(self, ln, alerts): + def tmpl_account_user_alerts(self, alerts, ln = CFG_SITE_LANG): """ - Displays all the alerts in the main "Your account" page + Information on the user's alerts for the "Your Account" page. Parameters: - + - 'alerts' *int* - The number of alerts - 'ln' *string* - The language to display the interface in - - - 'alerts' *array* - The existing alerts IDs ('id' + 'name' pairs) """ - # load the right message language _ = gettext_set_language(ln) - out = """<form name="displayalert" action="../youralerts/display" method="post"> - %(you_own)s: - <select name="id_alert"> - <option value="0">- %(alert_name)s -</option>""" % { - 'you_own' : _("You own the following alerts:"), - 'alert_name' : _("alert name"), - } - for alert in alerts : - out += """<option value="%(id)s">%(name)s</option>""" % \ - {'id': alert['id'], 'name': cgi.escape(alert['name'])} - out += """</select> - &nbsp;<input class="formbutton" type="submit" name="action" value="%(show)s" /> - </form>""" % { - 'show' : _("SHOW"), - } + if alerts > 0: + out = _('You have defined a total of %(x_url_open)s%(x_alerts_number)s alerts%(x_url_close)s.') % \ + {'x_url_open' : '<strong><a href="%s/youralerts/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_alerts_number': str(alerts), + 'x_url_close' : '</a></strong>'} + else: + out = _('You have not defined any alerts yet.') + + out += " " + _('You may define new alerts based on %(x_yoursearches_url_open)syour searches%(x_url_close)s or just by %(x_search_interface_url_open)ssearching for something new%(x_url_close)s.') % \ + {'x_yoursearches_url_open' : '<a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_search_interface_url_open' : '<a href="%s/?ln=%s">' % (CFG_SITE_URL, ln), + 'x_url_close' : '</a>',} + return out def tmpl_input_alert(self, @@ -141,9 +136,7 @@ def tmpl_input_alert(self, old_id_basket, id_basket, id_query, - is_active, - guest, - guesttxt): + is_active): """ Displays an alert adding form. @@ -170,10 +163,6 @@ def tmpl_input_alert(self, - 'id_query' *string* - The id of the query associated to this alert - 'is_active' *boolean* - is the alert active or not - - - 'guest' *bool* - If the user is a guest user - - - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) """ # load the right message language @@ -274,9 +263,6 @@ def tmpl_input_alert(self, out += '<input type="hidden" name="old_idb" value="%s" />' % old_id_basket out += "</form>" - if guest: - out += guesttxt - return out def tmpl_youralerts_display(self, @@ -776,10 +762,6 @@ def tmpl_youralerts_popular(self, - 'args' *string* - The query string - 'textargs' *string* - The textual description of the query string - - - 'guest' *bool* - If the user is a guest user - - - 'guesttxt' *string* - The HTML content of the warning box for guest users (produced by webaccount.tmpl_warning_guest_user) """ # load the right message language diff --git a/modules/webbasket/lib/webbasket.py b/modules/webbasket/lib/webbasket.py index 73710571b5..8d39f1045b 100644 --- a/modules/webbasket/lib/webbasket.py +++ b/modules/webbasket/lib/webbasket.py @@ -2573,30 +2573,15 @@ def create_webbasket_navtrail(uid, return out -def account_list_baskets(uid, ln=CFG_SITE_LANG): - """Display baskets informations on account page""" - _ = gettext_set_language(ln) +def account_user_baskets(uid, ln=CFG_SITE_LANG): + """ + Display baskets informations on account page + """ + (personal, group, external) = db.count_baskets(uid) - link = '<a href="%s">%s</a>' - base_url = CFG_SITE_URL + '/yourbaskets/display?category=%s&amp;ln=' + ln - personal_text = personal - if personal: - url = base_url % CFG_WEBBASKET_CATEGORIES['PRIVATE'] - personal_text = link % (url, personal_text) - group_text = group - if group: - url = base_url % CFG_WEBBASKET_CATEGORIES['GROUP'] - group_text = link % (url, group_text) - external_text = external - if external: - url = base_url % CFG_WEBBASKET_CATEGORIES['EXTERNAL'] - else: - url = CFG_SITE_URL + '/yourbaskets/list_public_baskets?ln=' + ln - external_text = link % (url, external_text) - out = _("You have %(x_nb_perso)s personal baskets and are subscribed to %(x_nb_group)s group baskets and %(x_nb_public)s other users public baskets.") %\ - {'x_nb_perso': personal_text, - 'x_nb_group': group_text, - 'x_nb_public': external_text} + + out = webbasket_templates.tmpl_account_user_baskets(personal, group, external, ln) + return out def page_start(req, of='xm'): diff --git a/modules/webbasket/lib/webbasket_dblayer.py b/modules/webbasket/lib/webbasket_dblayer.py index 4dd7f37949..d7fc581a67 100644 --- a/modules/webbasket/lib/webbasket_dblayer.py +++ b/modules/webbasket/lib/webbasket_dblayer.py @@ -127,23 +127,39 @@ ########################## General functions ################################## def count_baskets(uid): - """Return (nb personal baskets, nb group baskets, nb external - baskets) tuple for given user""" - query1 = "SELECT COUNT(id) FROM bskBASKET WHERE id_owner=%s" - res1 = run_sql(query1, (int(uid),)) - personal = __wash_sql_count(res1) - query2 = """SELECT count(ugbsk.id_bskbasket) - FROM usergroup_bskBASKET ugbsk LEFT JOIN user_usergroup uug - ON ugbsk.id_usergroup=uug.id_usergroup - WHERE uug.id_user=%s AND uug.user_status!=%s - GROUP BY ugbsk.id_usergroup""" - params = (int(uid), CFG_WEBSESSION_USERGROUP_STATUS['PENDING']) - res2 = run_sql(query2, params) - if len(res2): - groups = reduce(lambda x, y: x + y, map(lambda x: x[0], res2)) + """ + Return (number of personal baskets, + number of group baskets, + number of external baskets) + tuple for the given user based on their user id. + """ + + # TODO: Maybe put this in a try..except ? + uid = int(uid) + + query_personal = """SELECT COUNT(id) + FROM bskBASKET + WHERE id_owner=%s""" + params_personal = (uid,) + res_personal = run_sql(query_personal, params_personal) + personal = __wash_sql_count(res_personal) + + query_group = """ SELECT COUNT(ugbsk.id_bskbasket) + FROM usergroup_bskBASKET ugbsk + LEFT JOIN user_usergroup uug + ON ugbsk.id_usergroup = uug.id_usergroup + WHERE uug.id_user = %s + AND uug.user_status != %s + GROUP BY ugbsk.id_usergroup""" + params_group = (uid, CFG_WEBSESSION_USERGROUP_STATUS['PENDING']) + res_group = run_sql(query_group, params_group) + if len(res_group): + groups = reduce(lambda x, y: x + y, map(lambda x: x[0], res_group)) else: groups = 0 + external = count_external_baskets(uid) + return (personal, groups, external) def check_user_owns_baskets(uid, bskids): @@ -1567,13 +1583,16 @@ def get_all_external_basket_ids_and_names(uid): def count_external_baskets(uid): """Returns the number of external baskets the user is subscribed to.""" + # TODO: Maybe put this in a try..except ? + uid = int(uid) + query = """ SELECT COUNT(ubsk.id_bskBASKET) FROM user_bskBASKET ubsk LEFT JOIN bskBASKET bsk ON (bsk.id=ubsk.id_bskBASKET AND ubsk.id_user=%s) WHERE bsk.id_owner!=%s""" - params = (int(uid), int(uid)) + params = (uid, uid) res = run_sql(query, params) diff --git a/modules/webbasket/lib/webbasket_templates.py b/modules/webbasket/lib/webbasket_templates.py index 77d4c290a5..3d82ad8b5f 100644 --- a/modules/webbasket/lib/webbasket_templates.py +++ b/modules/webbasket/lib/webbasket_templates.py @@ -4185,6 +4185,34 @@ def tmpl_create_export_as_list(self, return out + def tmpl_account_user_baskets(self, personal, group, external, ln = CFG_SITE_LANG): + """ + Information on the user's baskets for the "Your Account" page. + """ + + _ = gettext_set_language(ln) + + if (personal, group, external) == (0, 0, 0): + out = _("You have not created any personal baskets yet, you are not part of any group baskets and you have not subscribed to any public baskets.") + else: + x_generic_url_open = '<a href="%s/yourbaskets/display?category=%%s&amp;ln=%s">' % (CFG_SITE_SECURE_URL, ln) + out = _("You have %(x_personal_url_open)s%(x_personal_nb)s personal baskets%(x_personal_url_close)s, you are part of %(x_group_url_open)s%(x_group_nb)s group baskets%(x_group_url_close)s and you are subscribed to %(x_public_url_open)s%(x_public_nb)s public baskets%(x_public_url_close)s.") % \ + {'x_personal_url_open' : '<strong>%s' % (personal > 0 and x_generic_url_open % (CFG_WEBBASKET_CATEGORIES['PRIVATE'],) or '',), + 'x_personal_nb' : str(personal), + 'x_personal_url_close' : '%s</strong>' % (personal > 0 and '</a>' or '',), + 'x_group_url_open' : '<strong>%s' % (group > 0 and x_generic_url_open % (CFG_WEBBASKET_CATEGORIES['GROUP'],) or '',), + 'x_group_nb' : str(group), + 'x_group_url_close' : '%s</strong>' % (group > 0 and '</a>' or '',), + 'x_public_url_open' : '<strong>%s' % (external > 0 and x_generic_url_open % (CFG_WEBBASKET_CATEGORIES['EXTERNAL'],) or '',), + 'x_public_nb' : str(external), + 'x_public_url_close' : '%s</strong>' % (external > 0 and '</a>' or '',),} + + out += " " + _("You might be interested in looking through %(x_url_open)sall the public baskets%(x_url_close)s.") % \ + {'x_url_open' : '<a href="%s/yourbaskets/list_public_baskets?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a>',} + + return out + ############################################# ########## SUPPLEMENTARY FUNCTIONS ########## ############################################# diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index ce92ec10f0..3dd09d59e7 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -2190,3 +2190,20 @@ def perform_display_your_comments(user_info, nb_total_results=nb_total_results, nb_total_pages=nb_total_pages, ln=ln) + +def account_user_comments(uid, ln = CFG_SITE_LANG): + """ + Information on the user comments for the "Your Account" page. + """ + + query = """ SELECT COUNT(id) + FROM cmtRECORDCOMMENT + WHERE id_user = %s + AND star_score = 0""" + params = (uid,) + result = run_sql(query, params) + comments = result[0][0] + + out = webcomment_templates.tmpl_account_user_comments(comments, ln) + + return out diff --git a/modules/webcomment/lib/webcomment_templates.py b/modules/webcomment/lib/webcomment_templates.py index d47089e80c..a22000ce66 100644 --- a/modules/webcomment/lib/webcomment_templates.py +++ b/modules/webcomment/lib/webcomment_templates.py @@ -2612,3 +2612,20 @@ def tmpl_your_comments(self, user_info, comments, page_number=1, selected_order_ out += '<br/><div id="yourcommentsnavigationlinks">' + page_links + '</div>' return out + + def tmpl_account_user_comments(self, comments, ln = CFG_SITE_LANG): + """ + Information on the user's comments for the "Your Account" page. + """ + + _ = gettext_set_language(ln) + + if comments > 0: + out = _("You have submitted %(x_url_open)s%(x_comments)s comments%(x_url_close)s so far.") % \ + {'x_url_open' : '<strong><a href="%s/yourcomments/?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_comments' : str(comments), + 'x_url_close' : '</a></strong>',} + else: + out = _("You have not yet submitted any comment. Browse documents from the search interface and take part to discussions!") + + return out diff --git a/modules/webmessage/lib/webmessage.py b/modules/webmessage/lib/webmessage.py index 40d3a93673..1e5cd9f2f6 100644 --- a/modules/webmessage/lib/webmessage.py +++ b/modules/webmessage/lib/webmessage.py @@ -458,17 +458,20 @@ def listing(name1, name2): title = _("Your Messages") return (body, title, get_navtrail(ln)) -def account_new_mail(uid, ln=CFG_SITE_LANG): +def account_user_messages(uid, ln = CFG_SITE_LANG): """ - display new mail info for myaccount.py page. + Information on the user's messages for the "Your Account" page @param uid: user id (int) - @param ln: language - @return: html body + @param ln: interface language (str) + @return: information in HTML """ - nb_new_mail = db.get_nb_new_messages_for_user(uid) - total_mail = db.get_nb_readable_messages_for_user(uid) - return webmessage_templates.tmpl_account_new_mail(nb_new_mail, - total_mail, ln) + + total = db.get_nb_readable_messages_for_user(uid) + unread = db.get_nb_new_messages_for_user(uid) + + out = webmessage_templates.tmpl_account_user_messages(total, unread, ln) + + return out def get_navtrail(ln=CFG_SITE_LANG, title=""): """ diff --git a/modules/webmessage/lib/webmessage_templates.py b/modules/webmessage/lib/webmessage_templates.py index 35ff83843d..1f442344b9 100644 --- a/modules/webmessage/lib/webmessage_templates.py +++ b/modules/webmessage/lib/webmessage_templates.py @@ -35,7 +35,7 @@ create_year_selectbox from invenio.urlutils import create_html_link, create_url from invenio.htmlutils import escape_html -from invenio.config import CFG_SITE_URL, CFG_SITE_LANG +from invenio.config import CFG_SITE_URL, CFG_SITE_SECURE_URL, CFG_SITE_LANG from invenio.messages import gettext_set_language from invenio.webuser import get_user_info @@ -681,21 +681,28 @@ def tmpl_user_or_group_search(self, 'add_button' : add_button} return out - def tmpl_account_new_mail(self, nb_new_mail=0, total_mail=0, ln=CFG_SITE_LANG): + def tmpl_account_user_messages(self, total = 0, unread = 0, ln = CFG_SITE_LANG): """ - display infos about inbox (used by myaccount.py) - @param nb_new_mail: number of new mails + Information about the user's messages for the "Your Account" page. + @param total: total number of messages + @param unread: number of unread messages @param ln: language - return: html output. + return: information in HTML """ + _ = gettext_set_language(ln) - out = _("You have %(x_nb_new)s new messages out of %(x_nb_total)s messages") % \ - {'x_nb_new': '<b>' + str(nb_new_mail) + '</b>', - 'x_nb_total': create_html_link(CFG_SITE_URL + '/yourmessages/', - {'ln': ln}, - str(total_mail), - {}, - False, False)} - return out + '.' + if total > 0: + out = _("You have received a total of %(x_url_open)s%(x_total)s messages (%(x_unread)s unread)%(x_url_close)s.") % \ + {'x_url_open' : '<strong><a href="%s/yourmessages/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_total' : str(total), + 'x_unread' : str(unread), + 'x_url_close' : '</a></strong>'} + else: + out = _("You have not received any messages yet.") + + out += " " + _("You might be interested in %(x_url_open)swriting a new message%(x_url_close)s.") % \ + {'x_url_open' : '<a href="%s/yourmessages/write?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a>',} + return out diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index e6bdbc13ec..087dbdab8e 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5126,10 +5126,6 @@ def tmpl_yoursearches_display(self, @param guest: Whether the user is a guest or not @type guest: boolean - - @param guesttxt: The HTML content of the warning box for guest users - (produced by webaccount.tmpl_warning_guest_user) - @type guesttxt: string """ # Load the right language @@ -5259,6 +5255,26 @@ def tmpl_yoursearches_display(self, return out + def tmpl_account_user_searches(self, unique, total, ln = CFG_SITE_LANG): + """ + Information on the user's searches for the "Your Account" page + """ + + _ = gettext_set_language(ln) + + if unique > 0: + out = _("You have performed %(x_url_open)s%(unique)s unique searches%(x_url_close)s in a total of %(total)s searches.") % \ + {'x_url_open' : '<strong><a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'unique' : str(unique), + 'x_url_close' : '</a></strong>', + 'total' : str(total),} + else: + out = _("You have not searched for anything yet. You may want to start by the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ + {'x_url_open' : '<a href="%s/?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a>'} + + return out + def get_html_user_friendly_search_query_args(args, ln=CFG_SITE_LANG): """ diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index 0d5461ef25..605588187b 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -21,7 +21,6 @@ from invenio.config import CFG_SITE_LANG, CFG_SITE_SECURE_URL from invenio.dbquery import run_sql -from invenio.webaccount import warning_guest_user from invenio.messages import gettext_set_language from invenio.webuser import isGuestUser from urllib import quote @@ -151,31 +150,27 @@ def perform_request_yoursearches_display(uid, p=p, ln = ln) -def account_list_searches(uid, - ln=CFG_SITE_LANG): +def account_user_searches(uid, ln = CFG_SITE_LANG): """ - Display a short summary of the searches the user has performed. + Information on the user's searches for the "Your Account" page @param uid: The user id @type uid: int - @return: A short summary of the user searches. + @param ln: The interface language + @type ln: str + @return: A short summary on the user's searches. """ - # load the right language - _ = gettext_set_language(ln) - - query = """ SELECT COUNT(uq.id_query) - FROM user_query uq - WHERE uq.id_user=%s""" + # Calculate the number of total and unique queries + query = """ SELECT COUNT(id_query), + COUNT(DISTINCT(id_query)) + FROM user_query + WHERE id_user=%s""" params = (uid,) - result = run_sql(query, params, 1) - if result: - nb_queries_total = result[0][0] - else: - nb_queries_total = 0 + res = run_sql(query, params) + + total = res[0][0] + unique = res[0][1] - out = _("You have made %(x_nb)s queries. A %(x_url_open)sdetailed list%(x_url_close)s is available with a possibility to (a) view search results and (b) subscribe to an automatic email alerting service for these queries.") % \ - {'x_nb': nb_queries_total, - 'x_url_open': '<a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), - 'x_url_close': '</a>'} + out = websearch_templates.tmpl_account_user_searches(unique, total, ln) return out diff --git a/modules/websession/lib/webaccount.py b/modules/websession/lib/webaccount.py index 6e98dc14bc..bc4fca0403 100644 --- a/modules/websession/lib/webaccount.py +++ b/modules/websession/lib/webaccount.py @@ -47,6 +47,16 @@ from invenio import web_api_key +from invenio.webbasket import account_user_baskets +from invenio.webalert import account_user_alerts +from invenio.websearch_yoursearches import account_user_searches +from invenio.webmessage import account_user_messages +from invenio.webgroup import account_user_groups +from invenio.websubmit_templates import account_user_submissions +from invenio.websession_templates import account_user_approvals +from invenio.webcomment import account_user_comments +from invenio.websession_templates import account_user_tickets + def perform_info(req, ln): """Display the main features of CDS personalize""" uid = getUid(req) @@ -74,15 +84,19 @@ def perform_display_external_user_settings(settings, ln): html_settings += websession_templates.tmpl_external_setting(ln, key, value) return print_settings and websession_templates.tmpl_external_user_settings(ln, html_settings) or "" -def perform_youradminactivities(user_info, ln): - """Return text for the `Your Admin Activities' box. Analyze - whether user UID has some admin roles, and if yes, then print - suitable links for the actions he can do. If he's not admin, - print a simple non-authorized message.""" +def account_user_administration(user_info, ln = CFG_SITE_LANG): + """ + Information for for the "Your Admin Activities" box. + Analyzes whether user UID has some admin roles, and if yes, then returns + suitable links for the action they can do. If they are not admin, return + a simple non-authorized message. + """ + your_role_actions = acc_find_user_role_actions(user_info) your_roles = [] your_admin_activities = [] guest = int(user_info['guest']) + for (role, action) in your_role_actions: if role not in your_roles: your_roles.append(role) @@ -90,48 +104,68 @@ def perform_youradminactivities(user_info, ln): your_admin_activities.append(action) if SUPERADMINROLE in your_roles: - for action in ("runbibedit", "cfgbibformat", "cfgoaiharvest", "cfgoairepository", "cfgbibrank", "cfgbibindex", "cfgwebaccess", "cfgwebcomment", "cfgwebsearch", "cfgwebsubmiit", "cfgbibknowledge", "runbatchuploader"): + for action in ("runbibedit", + "cfgbibformat", + "cfgoaiharvest", + "cfgoairepository", + "cfgbibrank", + "cfgbibindex", + "cfgwebaccess", + "cfgwebcomment", + "cfgwebsearch", + "cfgwebsubmiit", + "cfgbibknowledge", + "runbatchuploader"): if action not in your_admin_activities: your_admin_activities.append(action) - return websession_templates.tmpl_account_adminactivities( - ln = ln, - uid = user_info['uid'], - guest = guest, - roles = your_roles, - activities = your_admin_activities, - ) + out = websession_templates.tmpl_account_adminactivities( + ln = ln, + uid = user_info['uid'], + guest = guest, + roles = your_roles, + activities = your_admin_activities) -def perform_display_account(req, username, bask, aler, sear, msgs, loan, grps, sbms, appr, admn, ln, comments): - """Display a dynamic page that shows the user's account.""" + return out - # load the right message language - _ = gettext_set_language(ln) +def perform_display_account( + uid, + user_info, + user_name, + user_baskets_p, + user_alerts_p, + user_searches_p, + user_messages_p, + user_loans_p, + user_groups_p, + user_submissions_p, + user_approvals_p, + user_administration_p, + user_comments_p, + user_tickets_p, + ln): + """ + Display a dynamic page that shows the user's account. + """ - uid = getUid(req) - user_info = collect_user_info(req) - #your account - if int(user_info['guest']): - user = "guest" - login = "%s/youraccount/login?ln=%s" % (CFG_SITE_SECURE_URL, ln) - accBody = _("You are logged in as guest. You may want to %(x_url_open)slogin%(x_url_close)s as a regular user.") %\ - {'x_url_open': '<a href="' + login + '">', - 'x_url_close': '</a>'} - accBody += "<br /><br />" - bask=aler=msgs=comments= _("The %(x_fmt_open)sguest%(x_fmt_close)s users need to %(x_url_open)sregister%(x_url_close)s first") %\ - {'x_fmt_open': '<strong class="headline">', - 'x_fmt_close': '</strong>', - 'x_url_open': '<a href="' + login + '">', - 'x_url_close': '</a>'} - sear= _("No queries found") - else: - user = username - accBody = websession_templates.tmpl_account_body( - ln = ln, - user = user, - ) + # Load the right message language + _ = gettext_set_language(ln) - #Display warnings if user is superuser + user_account = websession_templates.tmpl_account_body(ln = ln, user = user_name) + user_baskets = user_baskets_p and account_user_baskets(uid, ln) or None + user_alerts = user_alerts_p and account_user_alerts(uid, ln) or None + user_searches = user_searches_p and account_user_searches(uid, ln) or None + user_messages = user_messages_p and account_user_messages(uid, ln) or None + # TODO: Write a function that returns "Loans" information for the user + user_loans = user_loans_p and None or None + user_groups = user_groups_p and account_user_groups(uid, ln) or None + user_submissions = user_submissions_p and account_user_submissions(ln) or None + user_approvals = user_approvals_p and account_user_approvals(ln) or None + user_administration = user_administration_p and account_user_administration(user_info, ln) or None + user_comments = user_comments_p and account_user_comments(uid, ln) or None + user_tickets = user_tickets_p and account_user_tickets(ln) or None + + # Display warnings if user is superuser roles = acc_find_user_role_actions(user_info) warnings = "0" warning_list = [] @@ -150,26 +184,22 @@ def perform_display_account(req, username, bask, aler, sear, msgs, loan, grps, s warnings = "1" warning_list.append(email_autogenerated_warning) - #check if tickets ok - tickets = (acc_authorize_action(user_info, 'runbibedit')[0] == 0) - return websession_templates.tmpl_account_page( - ln = ln, - warnings = warnings, - warning_list = warning_list, - accBody = accBody, - baskets = bask, - alerts = aler, - searches = sear, - messages = msgs, - loans = loan, - groups = grps, - submissions = sbms, - approvals = appr, - tickets = tickets, - administrative = admn, - comments = comments, - ) + ln = ln, + warnings = warnings, + warning_list = warning_list, + account = user_account, + baskets = user_baskets, + alerts = user_alerts, + searches = user_searches, + messages = user_messages, + loans = user_loans, + groups = user_groups, + submissions = user_submissions, + approvals = user_approvals, + tickets = user_tickets, + administrative = user_administration, + comments = user_comments) def superuser_account_warnings(): """Check to see whether admin accounts have default / blank password etc. Returns a list""" @@ -258,18 +288,6 @@ def template_account(title, body, ln): body = body ) -def warning_guest_user(type, ln=CFG_SITE_LANG): - """It returns an alert message,showing that the user is a guest user and should log into the system.""" - - # load the right message language - _ = gettext_set_language(ln) - - return websession_templates.tmpl_warning_guest_user( - ln = ln, - type = type, - ) - - def perform_delete(ln): """Delete the account of the user, not implement yet.""" # TODO diff --git a/modules/websession/lib/webgroup.py b/modules/websession/lib/webgroup.py index e3105113b6..99a2e780b6 100644 --- a/modules/websession/lib/webgroup.py +++ b/modules/websession/lib/webgroup.py @@ -761,21 +761,31 @@ def perform_request_reject_member(uid, ln=ln) return body -def account_group(uid, ln=CFG_SITE_LANG): - """Display group info for myaccount.py page. +def account_user_groups(uid, ln=CFG_SITE_LANG): + """ + Information on the user's groups for the "Your Account" page @param uid: user id (int) - @param ln: language - @return: html body + @param ln: language (str) + @return: information in HTML """ - nb_admin_groups = db.count_nb_group_user(uid, + + nb_admin_groups = db.count_nb_group_user( + uid, CFG_WEBSESSION_USERGROUP_STATUS["ADMIN"]) - nb_member_groups = db.count_nb_group_user(uid, + + nb_member_groups = db.count_nb_group_user( + uid, CFG_WEBSESSION_USERGROUP_STATUS["MEMBER"]) + nb_total_groups = nb_admin_groups + nb_member_groups - return websession_templates.tmpl_group_info(nb_admin_groups, + + out = websession_templates.tmpl_account_user_groups( + nb_admin_groups, nb_member_groups, nb_total_groups, - ln=ln) + ln = ln) + + return out def get_navtrail(ln=CFG_SITE_LANG, title=""): """Gets the navtrail for title. diff --git a/modules/websession/lib/websession_templates.py b/modules/websession/lib/websession_templates.py index 91d89f4711..cf09bb43f2 100644 --- a/modules/websession/lib/websession_templates.py +++ b/modules/websession/lib/websession_templates.py @@ -686,14 +686,14 @@ def tmpl_account_body(self, ln, user): # load the right message language _ = gettext_set_language(ln) - out = _("You are logged in as %(x_user)s. You may want to a) %(x_url1_open)slogout%(x_url1_close)s; b) edit your %(x_url2_open)saccount settings%(x_url2_close)s.") %\ - {'x_user': user, - 'x_url1_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/logout?ln=' + ln + '">', + out = _("You are logged in as %(x_user)s. You may want to %(x_url1_open)sedit your account settings%(x_url1_close)s or %(x_url2_open)slogout%(x_url2_close)s.") %\ + {'x_user': "<strong>" + cgi.escape(user) + "</strong>", + 'x_url1_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/edit?ln=' + ln + '">', 'x_url1_close': '</a>', - 'x_url2_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/edit?ln=' + ln + '">', - 'x_url2_close': '</a>', - } - return out + "<br /><br />" + 'x_url2_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/logout?ln=' + ln + '">', + 'x_url2_close': '</a>',} + + return out def tmpl_account_template(self, title, body, ln, url): """ @@ -710,18 +710,32 @@ def tmpl_account_template(self, title, body, ln, url): - 'url' *string* - The URL to go to the proper section """ - out =""" - <table class="youraccountbox" width="90%%" summary="" > - <tr> - <th class="youraccountheader"><a href="%s">%s</a></th> - </tr> - <tr> - <td class="youraccountbody">%s</td> - </tr> - </table>""" % (url, title, body) + out = """ + <div class="youraccount_grid_column_content"> + <div class="youraccount_grid_column_title"><a href="%s">%s</a></div> + <div class="youraccount_grid_column_body">%s</div> + </div> + """ % (url, title, body) + return out - def tmpl_account_page(self, ln, warnings, warning_list, accBody, baskets, alerts, searches, messages, loans, groups, submissions, approvals, tickets, administrative, comments): + def tmpl_account_page( + self, + ln, + warnings, + warning_list, + account, + baskets, + alerts, + searches, + messages, + loans, + groups, + submissions, + approvals, + tickets, + administrative, + comments): """ Displays the your account page @@ -729,7 +743,7 @@ def tmpl_account_page(self, ln, warnings, warning_list, accBody, baskets, alerts - 'ln' *string* - The language to display the interface in - - 'accBody' *string* - The body of the heading block + - 'account' *string* - The body of the heading block - 'baskets' *string* - The body of the baskets block @@ -759,53 +773,88 @@ def tmpl_account_page(self, ln, warnings, warning_list, accBody, baskets, alerts if warnings == "1": out += self.tmpl_general_warnings(warning_list) - out += self.tmpl_account_template(_("Your Account"), accBody, ln, '/youraccount/edit?ln=%s' % ln) - if messages: - out += self.tmpl_account_template(_("Your Messages"), messages, ln, '/yourmessages/display?ln=%s' % ln) + # Store all the information to be displayed in the items list. + # Only store items that should be displayed. + # NOTE: The order in which we append items to the list matters! + items = [] + account is not None and items.append(self.tmpl_account_template( + _("Your Account"), account, ln, "/youraccount/edit?ln=%s" % ln)) + groups is not None and items.append(self.tmpl_account_template( + _("Your Groups"), groups, ln, "/yourgroups/display?ln=%s" % ln)) + administrative is not None and items.append(self.tmpl_account_template( + _("Your Administrative Activities"), administrative, ln, "/admin")) + messages is not None and items.append(self.tmpl_account_template( + _("Your Messages"), messages, ln, "/yourmessages/display?ln=%s" % ln)) + searches is not None and items.append(self.tmpl_account_template( + _("Your Searches"), searches, ln, "/yoursearches/display?ln=%s" % ln)) + alerts is not None and items.append(self.tmpl_account_template( + _("Your Alerts"), alerts, ln, "/youralerts/display?ln=%s" % ln)) + baskets is not None and items.append(self.tmpl_account_template( + _("Your Baskets"), baskets, ln, "/yourbaskets/display?ln=%s" % ln)) + comments is not None and items.append(self.tmpl_account_template( + _("Your Comments"), comments, ln, "/yourcomments/?ln=%s" % ln)) + submissions is not None and items.append(self.tmpl_account_template( + _("Your Submissions"), submissions, ln, "/yoursubmissions.py?ln=%s" % ln)) + approvals is not None and items.append(self.tmpl_account_template( + _("Your Approvals"), approvals, ln, "/yourapprovals.py?ln=%s" % ln)) + loans is not None and items.append(self.tmpl_account_template( + _("Your Loans"), loans, ln, "/yourloans/display?ln=%s" % ln)) + tickets is not None and items.append(self.tmpl_account_template( + _("Your Tickets"), tickets, ln, "/yourtickets?ln=%s" % ln)) + + # Prepare 3 more lists, 1 for each of the 3 columns + items_in_column_1_of_3 = [] + items_in_column_2_of_3 = [] + items_in_column_3_of_3 = [] + + # Sort-of-intelligently arrange the items in the 3 lists + while items: + # While there are still items to display, get the first item, + # removing it from the list of items to display. + item = items.pop(0) + # The following "1-liner" does the following (>= Python 2.5): + # * For each of the 3 lists (1 for each column) + # calculate the sum of the length of its items (see the lambda). + # The lenght of an itme is the literal length of the string + # to be displayed, which includes HTML code. This of course is + # not the most accurate way, but a good approximation and also + # very efficient. + # * Pick the list that has the smallest sum. + # * Append the current item to that list. + min(items_in_column_1_of_3, + items_in_column_2_of_3, + items_in_column_3_of_3, + key = lambda l: sum([len(i) for i in l]) + ).append(item) + + # Finally, create the HTML code for the entire grid. + out += """ + <div class="youraccount_grid"> + <div class="youraccount_grid_column_1_3">""" - if loans: - out += self.tmpl_account_template(_("Your Loans"), loans, ln, '/yourloans/display?ln=%s' % ln) + for item_in_column_1_of_3 in items_in_column_1_of_3: + out += item_in_column_1_of_3 - if baskets: - out += self.tmpl_account_template(_("Your Baskets"), baskets, ln, '/yourbaskets/display?ln=%s' % ln) + out += """ + </div> + <div class="youraccount_grid_column_1_3">""" - if comments: - comments_description = _("You can consult the list of %(x_url_open)syour comments%(x_url_close)s submitted so far.") - comments_description %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yourcomments/?ln=' + ln + '">', - 'x_url_close': '</a>'} - out += self.tmpl_account_template(_("Your Comments"), comments_description, ln, '/yourcomments/?ln=%s' % ln) - if alerts: - out += self.tmpl_account_template(_("Your Alert Searches"), alerts, ln, '/youralerts/display?ln=%s' % ln) + for item_in_column_2_of_3 in items_in_column_2_of_3: + out += item_in_column_2_of_3 - if searches: - out += self.tmpl_account_template(_("Your Searches"), searches, ln, '/yoursearches/display?ln=%s' % ln) + out += """ + </div> + <div class="youraccount_grid_column_1_3"> + """ + + for item_in_column_3_of_3 in items_in_column_3_of_3: + out += item_in_column_3_of_3 + + out += """ + </div> + </div> + """ - if groups: - groups_description = _("You can consult the list of %(x_url_open)syour groups%(x_url_close)s you are administering or are a member of.") - groups_description %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yourgroups/display?ln=' + ln + '">', - 'x_url_close': '</a>'} - out += self.tmpl_account_template(_("Your Groups"), groups_description, ln, '/yourgroups/display?ln=%s' % ln) - - if submissions: - submission_description = _("You can consult the list of %(x_url_open)syour submissions%(x_url_close)s and inquire about their status.") - submission_description %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yoursubmissions.py?ln=' + ln + '">', - 'x_url_close': '</a>'} - out += self.tmpl_account_template(_("Your Submissions"), submission_description, ln, '/yoursubmissions.py?ln=%s' % ln) - - if approvals: - approval_description = _("You can consult the list of %(x_url_open)syour approvals%(x_url_close)s with the documents you approved or refereed.") - approval_description %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yourapprovals.py?ln=' + ln + '">', - 'x_url_close': '</a>'} - out += self.tmpl_account_template(_("Your Approvals"), approval_description, ln, '/yourapprovals.py?ln=%s' % ln) - - #check if this user might have tickets - if tickets: - ticket_description = _("You can consult the list of %(x_url_open)syour tickets%(x_url_close)s.") - ticket_description %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yourtickets?ln=' + ln + '">', - 'x_url_close': '</a>'} - out += self.tmpl_account_template(_("Your Tickets"), ticket_description, ln, '/yourtickets?ln=%s' % ln) - if administrative: - out += self.tmpl_account_template(_("Your Administrative Activities"), administrative, ln, '/admin') return out def tmpl_account_emailMessage(self, ln, msg): @@ -1223,25 +1272,19 @@ def tmpl_register_page(self, ln, referer, level): def tmpl_account_adminactivities(self, ln, uid, guest, roles, activities): """ - Displays the admin activities block for this user + Displays the admin activities block for this user. Parameters: - - 'ln' *string* - The language to display the interface in - - 'uid' *string* - The used id - - 'guest' *boolean* - If the user is guest - - 'roles' *array* - The current user roles - - 'activities' *array* - The user allowed activities """ # load the right message language _ = gettext_set_language(ln) - out = "" # guest condition if guest: return _("You seem to be a guest user. You have to %(x_url_open)slogin%(x_url_close)s first.") % \ @@ -1250,66 +1293,70 @@ def tmpl_account_adminactivities(self, ln, uid, guest, roles, activities): # no rights condition if not roles: - return "<p>" + _("You are not authorized to access administrative functions.") + "</p>" + return _("You are not authorized to access administrative functions.") # displaying form - out += "<p>" + _("You are enabled to the following roles: %(x_role)s.") % {'x_role': ('<em>' + ", ".join(roles) + "</em>")} + '</p>' + out = _("You are enabled to the following roles: %(x_role)s.") % \ + {'x_role': ('<em>' + ", ".join(roles) + "</em>")} if activities: # print proposed links: activities.sort(lambda x, y: cmp(x.lower(), y.lower())) - tmp_out = '' + tmp_out = activities and '<ul>' or '' for action in activities: if action == "runbibedit": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/%s/edit/">%s</a>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Editor")) + tmp_out += """<li><a href="%s/%s/edit/">%s</a></li>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Editor")) if action == "runbibeditmulti": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/%s/multiedit/">%s</a>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Multi-Record Editor")) + tmp_out += """<li><a href="%s/%s/multiedit/">%s</a></li>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Multi-Record Editor")) if action == "runauthorlist": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/authorlist/">%s</a>""" % (CFG_SITE_URL, _("Run Author List Manager")) + tmp_out += """<li><a href="%s/authorlist/">%s</a></li>""" % (CFG_SITE_URL, _("Run Author List Manager")) if action == "runbibcirculation": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/bibcirculation/bibcirculationadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Run BibCirculation")) + tmp_out += """<li><a href="%s/admin/bibcirculation/bibcirculationadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Run BibCirculation")) if action == "runbibmerge": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/%s/merge/">%s</a>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Merger")) + tmp_out += """<li><a href="%s/%s/merge/">%s</a></li>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run Record Merger")) if action == "runbibswordclient": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/%s/bibsword/">%s</a>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run BibSword Client")) + tmp_out += """<li><a href="%s/%s/bibsword/">%s</a></li>""" % (CFG_SITE_URL, CFG_SITE_RECORD, _("Run BibSword Client")) if action == "runbatchuploader": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/batchuploader/metadata?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Run Batch Uploader")) + tmp_out += """<li><a href="%s/batchuploader/metadata?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Run Batch Uploader")) if action == "cfgbibformat": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/bibformat/bibformatadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure BibFormat")) + tmp_out += """<li><a href="%s/admin/bibformat/bibformatadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure BibFormat")) if action == "cfgbibknowledge": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/kb?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure BibKnowledge")) + tmp_out += """<li><a href="%s/kb?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure BibKnowledge")) if action == "cfgoaiharvest": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/oaiharvest/oaiharvestadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure OAI Harvest")) + tmp_out += """<li><a href="%s/admin/oaiharvest/oaiharvestadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure OAI Harvest")) if action == "cfgoairepository": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/oairepository/oairepositoryadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure OAI Repository")) + tmp_out += """<li><a href="%s/admin/oairepository/oairepositoryadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure OAI Repository")) if action == "cfgbibindex": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/bibindex/bibindexadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure BibIndex")) + tmp_out += """<li><a href="%s/admin/bibindex/bibindexadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure BibIndex")) if action == "cfgbibrank": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/bibrank/bibrankadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure BibRank")) + tmp_out += """<li><a href="%s/admin/bibrank/bibrankadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure BibRank")) if action == "cfgwebaccess": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/webaccess/webaccessadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebAccess")) + tmp_out += """<li><a href="%s/admin/webaccess/webaccessadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebAccess")) if action == "cfgwebcomment": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/webcomment/webcommentadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebComment")) + tmp_out += """<li><a href="%s/admin/webcomment/webcommentadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebComment")) if action == "cfgweblinkback": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/weblinkback/weblinkbackadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebLinkback")) + tmp_out += """<li><a href="%s/admin/weblinkback/weblinkbackadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebLinkback")) if action == "cfgwebjournal": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/webjournal/webjournaladmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebJournal")) + tmp_out += """<li><a href="%s/admin/webjournal/webjournaladmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebJournal")) if action == "cfgwebsearch": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/websearch/websearchadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebSearch")) + tmp_out += """<li><a href="%s/admin/websearch/websearchadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebSearch")) if action == "cfgwebsubmit": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/websubmit/websubmitadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure WebSubmit")) + tmp_out += """<li><a href="%s/admin/websubmit/websubmitadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure WebSubmit")) if action == "runbibdocfile": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/%s/managedocfiles?ln=%s">%s</a>""" % (CFG_SITE_URL, CFG_SITE_RECORD, ln, _("Run Document File Manager")) + tmp_out += """<li><a href="%s/%s/managedocfiles?ln=%s">%s</a></li>""" % (CFG_SITE_URL, CFG_SITE_RECORD, ln, _("Run Document File Manager")) if action == "cfgbibsort": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/admin/bibsort/bibsortadmin.py?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Configure BibSort")) + tmp_out += """<li><a href="%s/admin/bibsort/bibsortadmin.py?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Configure BibSort")) if action == "runinfomanager": - tmp_out += """<br />&nbsp;&nbsp;&nbsp; <a href="%s/info/manage?ln=%s">%s</a>""" % (CFG_SITE_URL, ln, _("Run Info Space Manager")) + tmp_out += """<li><a href="%s/info/manage?ln=%s">%s</a></li>""" % (CFG_SITE_URL, ln, _("Run Info Space Manager")) + tmp_out += activities and '</ul>' or '' + if tmp_out: - out += _("Here are some interesting web admin links for you:") + tmp_out + out += "<br /><br />" + _("Here are some interesting web admin links for you:") + tmp_out + + out += _("For the complete list of administrative activities, see the %(x_url_open)sAdmin Area%(x_url_close)s.") %\ + {'x_url_open' : '<a href="%s/help/admin?ln=%s">' % (CFG_SITE_URL, ln), + 'x_url_close' : '</a>'} - out += "<br />" + _("For more admin-level activities, see the complete %(x_url_open)sAdmin Area%(x_url_close)s.") %\ - {'x_url_open': '<a href="' + CFG_SITE_URL + '/help/admin?ln=' + ln + '">', - 'x_url_close': '</a>'} return out def tmpl_create_userinfobox(self, ln, url_referer, guest, username, submitter, referee, admin, usebaskets, usemessages, usealerts, usegroups, useloans, usestats): @@ -2595,22 +2642,40 @@ def tmpl_delete_msg(self, body += '<br />' return subject, body - def tmpl_group_info(self, nb_admin_groups=0, nb_member_groups=0, nb_total_groups=0, ln=CFG_SITE_LANG): - """ - display infos about groups (used by myaccount.py) - @param nb_admin_group: number of groups the user is admin of - @param nb_member_group: number of groups the user is member of - @param total_group: number of groups the user belongs to + def tmpl_account_user_groups( + self, + nb_admin_groups = 0, + nb_member_groups = 0, + nb_total_groups = 0, + ln = CFG_SITE_LANG): + """ + Information on the user's groups for the "Your Account" page + @param nb_admin_groups: number of groups the user is admin of + @param nb_member_groups: number of groups the user is member of + @param nb_total_groups: number of groups the user belongs to @param ln: language return: html output. """ + _ = gettext_set_language(ln) - out = _("You can consult the list of %(x_url_open)s%(x_nb_total)i groups%(x_url_close)s you are subscribed to (%(x_nb_member)i) or administering (%(x_nb_admin)i).") - out %= {'x_url_open': '<a href="' + CFG_SITE_URL + '/yourgroups/display?ln=' + ln + '">', - 'x_nb_total': nb_total_groups, - 'x_url_close': '</a>', - 'x_nb_admin': nb_admin_groups, - 'x_nb_member': nb_member_groups} + + if nb_total_groups > 0: + out = _("You are a member of %(x_url_open)s%(x_total)s groups%(x_url_close)s") % \ + {'x_url_open' : '<strong><a href="%s/yourgroups/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_total' : str(nb_total_groups), + 'x_url_close' : '</a></strong>',} + if nb_admin_groups > 0: + out += ", " + _("%(x_admin)s of which you administer") % \ + {'x_admin' : str(nb_admin_groups),} + out += "." + else: + out = _("You are not member of any groups.") + + out += " " + _("You might be interested in %(x_join_url_open)sjoining a group%(x_url_close)s or %(x_create_url_open)screating a new one%(x_url_close)s.") % \ + {'x_join_url_open' : '<a href="%s/yourgroups/join?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_create_url_open' : '<a href="%s/yourgroups/create?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a>',} + return out def tmpl_general_warnings(self, warning_list, ln=CFG_SITE_LANG): @@ -2877,3 +2942,29 @@ def construct_button(provider, size, button_class): } </script>""" return out + +def account_user_approvals(ln = CFG_SITE_LANG): + """ + Information on the user approvals for the "Your Account" page. + """ + + _ = gettext_set_language(ln) + + out = _("You can review all the documents you approved or refereed in %(x_url_open)syour approvals%(x_url_close)s.") % \ + {'x_url_open' : '<strong><a href="%s/yourapprovals.py?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a></strong>',} + + return out + +def account_user_tickets(ln = CFG_SITE_LANG): + """ + Information on the user tickets for the "Your Account" page. + """ + + _ = gettext_set_language(ln) + + out = _("You can review %(x_url_open)syour tickets%(x_url_close)s.") % \ + {'x_url_open': '<strong><a href="%s/yourtickets?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close': '</a></strong>',} + + return out diff --git a/modules/websession/lib/websession_webinterface.py b/modules/websession/lib/websession_webinterface.py index be00e2aa34..73599cb9c1 100644 --- a/modules/websession/lib/websession_webinterface.py +++ b/modules/websession/lib/websession_webinterface.py @@ -44,11 +44,7 @@ from invenio import webuser from invenio.webpage import page from invenio import webaccount -from invenio import webbasket -from invenio import webalert -from invenio import websearch_yoursearches from invenio.dbquery import run_sql -from invenio.webmessage import account_new_mail from invenio.access_control_engine import acc_authorize_action from invenio.webinterface_handler import wash_urlargd, WebInterfaceDirectory from invenio import webinterface_handler_config as apache @@ -74,13 +70,10 @@ from invenio import web_api_key - import invenio.template websession_templates = invenio.template.load('websession') bibcatalog_templates = invenio.template.load('bibcatalog') - - class WebInterfaceYourAccountPages(WebInterfaceDirectory): _exports = ['', 'edit', 'change', 'lost', 'display', @@ -227,39 +220,50 @@ def display(self, req, form): navmenuid='youraccount') if webuser.isGuestUser(uid): - return page(title=_("Your Account"), - body=webaccount.perform_info(req, args['ln']), - description="%s Personalize, Main page" % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), - keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), - uid=uid, - req=req, + # TODO: use CFG_WEBSESSION_DIFFERENTIATE_BETWEEN_GUESTS to decide whether to redirect the user to + # the login page or show them the page that is currently shown. + return page(title = _("Your Account"), + body = webaccount.perform_info(req, args['ln']), + description = "%s Personalize, Main page" % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), + keywords = _("%s, personalize") % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), + uid = uid, + req = req, secure_page_p = 1, - language=args['ln'], - lastupdated=__lastupdated__, - navmenuid='youraccount') + language = args['ln'], + lastupdated = __lastupdated__, + navmenuid = 'youraccount') - username = webuser.get_nickname_or_email(uid) + user_name = webuser.get_nickname_or_email(uid) user_info = webuser.collect_user_info(req) - bask = user_info['precached_usebaskets'] and webbasket.account_list_baskets(uid, ln=args['ln']) or '' - aler = user_info['precached_usealerts'] and webalert.account_list_alerts(uid, ln=args['ln']) or '' - sear = websearch_yoursearches.account_list_searches(uid, ln=args['ln']) - msgs = user_info['precached_usemessages'] and account_new_mail(uid, ln=args['ln']) or '' - grps = user_info['precached_usegroups'] and webgroup.account_group(uid, ln=args['ln']) or '' - appr = user_info['precached_useapprove'] - sbms = user_info['precached_viewsubmissions'] - comments = user_info['precached_sendcomments'] - loan = '' - admn = webaccount.perform_youradminactivities(user_info, args['ln']) - return page(title=_("Your Account"), - body=webaccount.perform_display_account(req, username, bask, aler, sear, msgs, loan, grps, sbms, appr, admn, args['ln'], comments), - description="%s Personalize, Main page" % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), - keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), - uid=uid, - req=req, - secure_page_p = 1, - language=args['ln'], - lastupdated=__lastupdated__, - navmenuid='youraccount') + + body = webaccount.perform_display_account( + uid, + user_info, + user_name, + user_baskets_p = user_info['precached_usebaskets'], + user_alerts_p = user_info['precached_usealerts'], + user_searches_p = True, + user_messages_p = user_info['precached_usemessages'], + user_loans_p = False, + user_groups_p = user_info['precached_usegroups'], + user_submissions_p = user_info['precached_viewsubmissions'], + user_approvals_p = user_info['precached_useapprove'], + user_administration_p = True, + user_comments_p = user_info['precached_sendcomments'], + user_tickets_p = (acc_authorize_action(user_info, 'runbibedit')[0] == 0), + ln = args['ln']) + + return page( + title = _("Your Account"), + body = body, + description = "%s Personalize, Main page" % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), + keywords = _("%s, personalize") % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), + uid = uid, + req = req, + secure_page_p = 1, + language = args['ln'], + lastupdated = __lastupdated__, + navmenuid = 'youraccount') def apikey(self, req, form): args = wash_urlargd(form, { @@ -713,7 +717,7 @@ def youradminactivities(self, req, form): navmenuid='admin') return page(title=_("Your Administrative Activities"), - body=webaccount.perform_youradminactivities(user_info, args['ln']), + body=webaccount.account_user_administration(user_info, args['ln']), navtrail="""<a class="navtrail" href="%s/youraccount/display?ln=%s">""" % (CFG_SITE_SECURE_URL, args['ln']) + _("Your Account") + """</a>""", description="%s Personalize, Main page" % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), keywords=_("%s, personalize") % CFG_SITE_NAME_INTL.get(args['ln'], CFG_SITE_NAME), diff --git a/modules/webstyle/css/invenio.css b/modules/webstyle/css/invenio.css index 2849d124bb..791f07dc10 100644 --- a/modules/webstyle/css/invenio.css +++ b/modules/webstyle/css/invenio.css @@ -829,38 +829,50 @@ a.google:hover { text-align: left; vertical-align: top; } -.youraccountbox { - color: #000; - background: #fff; - padding: 1px; - margin: 5px 0px 5px 0px; - border-collapse: collapse; - border-top: 1px solid #fc0; + +div.youraccount_grid { + width: 100%; } -.youraccountheader { - color: #333; - background: #ffc; - font-weight: normal; - font-size: small; - vertical-align: top; - text-align: left; + +div.youraccount_grid:after { + content: ""; + display: table; + clear: both; } -.youraccountbody { - color: #333; - background: #fff; - padding: 0px 5px 0px 5px; - margin-bottom:5px; - font-size: small; - text-align: left; - vertical-align: top; + +div.youraccount_grid_column_1_3 { + width: 33.33%; + float: left; } -th.youraccountheader a:link, th.youraccountheader a:visited { - color:#000000; - text-decoration:none; + +div.youraccount_grid_column_content { + margin: 7px 7px 14px 7px; + border: 1px solid #FFCC00; + /*border-radius: 0px 0px 15px 0px;*/ } -th.youraccountheader a:hover { - text-decoration:underline; + +div.youraccount_grid_column_title { + border-bottom: 1px solid #FFCC00; + background-color: #FFFFCC; + padding: 7px; } + +div.youraccount_grid_column_title a, div.youraccount_grid_column_title a:link, div.youraccount_grid_column_title a:visited, div.youraccount_grid_column_title a:active { + color: black; + text-decoration: none; + font-weight: bold; +} + +div.youraccount_grid_column_title a:hover { + color: black; + text-decoration: underline; + font-weight: bold; +} + +div.youraccount_grid_column_body { + padding: 10px; +} + .adminbox { color: #000; background: #f1f1f1; diff --git a/modules/websubmit/lib/websubmit_templates.py b/modules/websubmit/lib/websubmit_templates.py index 44cf5f8ca8..91eb13cd63 100644 --- a/modules/websubmit/lib/websubmit_templates.py +++ b/modules/websubmit/lib/websubmit_templates.py @@ -2903,3 +2903,20 @@ def displaycplxdoc_displayauthaction(action, linkText): "action" : action, "linkText" : linkText } + +def account_user_submissions(ln = CFG_SITE_LANG): + """ + Information on the user submissions for the "Your Account" page. + """ + + # TODO: give more information, such as the number of submissions the user has, + # how many are finished, pending etc. For that we need to pass the user id and + # introduce another function (in websubmit.py?) that calculates those numbers. + + _ = gettext_set_language(ln) + + out = _("You can review the status of all %(x_url_open)syour submissions%(x_url_close)s.") % \ + {'x_url_open' : '<strong><a href="%s/yoursubmissions.py?ln=%s">' % (CFG_SITE_SECURE_URL, ln), + 'x_url_close' : '</a></strong>',} + + return out From fab4db49e02124d1f18e2bb547f5ed7f247e7ca4 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Wed, 26 Mar 2014 11:42:26 +0100 Subject: [PATCH 68/83] General: fix kwalitee reported issues * Fixes various warnings and errors reported by kwalitee. --- modules/webalert/lib/webalert.py | 1 - modules/webalert/lib/webalert_templates.py | 5 +- modules/webbasket/lib/webbasket.py | 35 ++-- modules/webbasket/lib/webbasket_dblayer.py | 161 ++---------------- modules/webbasket/lib/webbasket_templates.py | 21 +-- .../webbasket/lib/webbasket_webinterface.py | 2 +- modules/webcomment/lib/webcomment.py | 27 ++- modules/webmessage/lib/webmessage.py | 2 +- .../websearch/lib/websearch_yoursearches.py | 3 +- modules/websession/lib/webaccount.py | 8 +- modules/websession/lib/webgroup.py | 1 - .../websession/lib/websession_templates.py | 22 +-- .../websession/lib/websession_webinterface.py | 8 +- 13 files changed, 70 insertions(+), 226 deletions(-) diff --git a/modules/webalert/lib/webalert.py b/modules/webalert/lib/webalert.py index 477dc4d8ca..791b275fc7 100644 --- a/modules/webalert/lib/webalert.py +++ b/modules/webalert/lib/webalert.py @@ -25,7 +25,6 @@ from invenio.config import CFG_SITE_LANG from invenio.dbquery import run_sql -from invenio.webuser import isGuestUser from invenio.errorlib import register_exception from invenio.webbasket_dblayer import \ check_user_owns_baskets, \ diff --git a/modules/webalert/lib/webalert_templates.py b/modules/webalert/lib/webalert_templates.py index 34d8939465..3b1054a372 100644 --- a/modules/webalert/lib/webalert_templates.py +++ b/modules/webalert/lib/webalert_templates.py @@ -134,7 +134,6 @@ def tmpl_input_alert(self, notification, baskets, old_id_basket, - id_basket, id_query, is_active): """ @@ -158,8 +157,6 @@ def tmpl_input_alert(self, - 'old_id_basket' *string* - The id of the previous basket of this alert - - 'id_basket' *string* - The id of the basket of this alert - - 'id_query' *string* - The id of the query associated to this alert - 'is_active' *boolean* - is the alert active or not @@ -832,7 +829,7 @@ def tmpl_personal_basket_select_element( 'selected': bskid == '' and ' selected="selected"' or '', 'label': _("Don't store results in basket...")} - # Create the <optgroup>s and <option>s for the user personal baskets. + # Create the optgroups and options for the user personal baskets. if personal_baskets_list: out += """ <optgroup label="%s">""" % ('* ' + _('Your personal baskets') + ' *',) diff --git a/modules/webbasket/lib/webbasket.py b/modules/webbasket/lib/webbasket.py index 8d39f1045b..fa24d325d7 100644 --- a/modules/webbasket/lib/webbasket.py +++ b/modules/webbasket/lib/webbasket.py @@ -1053,7 +1053,7 @@ def perform_request_search(uid, p="", b="", n=0, - #format='xm', + #record_format='xm', ln=CFG_SITE_LANG): """Search the baskets... @param uid: user id @@ -1160,12 +1160,12 @@ def perform_request_search(uid, # The search format for external records. This means in which format will # the external records be fetched from the database to be searched then. - format = 'xm' + record_format = 'xm' ### Calculate the search results for the user's personal baskets ### if b.startswith("P") or not b: personal_search_results = {} - personal_items = db.get_all_items_in_user_personal_baskets(uid, selected_topic, format) + personal_items = db.get_all_items_in_user_personal_baskets(uid, selected_topic, record_format) personal_local_items = personal_items[0] personal_external_items = personal_items[1] personal_external_items_xml_records = {} @@ -1239,7 +1239,7 @@ def perform_request_search(uid, ### Calculate the search results for the user's group baskets ### if b.startswith("G") or not b: group_search_results = {} - group_items = db.get_all_items_in_user_group_baskets(uid, selected_group_id, format) + group_items = db.get_all_items_in_user_group_baskets(uid, selected_group_id, record_format) group_local_items = group_items[0] group_external_items = group_items[1] group_external_items_xml_records = {} @@ -1328,7 +1328,7 @@ def perform_request_search(uid, ### Calculate the search results for the user's public baskets ### if b.startswith("E") or not b: public_search_results = {} - public_items = db.get_all_items_in_user_public_baskets(uid, format) + public_items = db.get_all_items_in_user_public_baskets(uid, record_format) public_local_items = public_items[0] public_external_items = public_items[1] public_external_items_xml_records = {} @@ -1411,7 +1411,7 @@ def perform_request_search(uid, ### Calculate the search results for all the public baskets ### if b.startswith("A"): all_public_search_results = {} - all_public_items = db.get_all_items_in_all_public_baskets(format) + all_public_items = db.get_all_items_in_all_public_baskets(record_format) all_public_local_items = all_public_items[0] all_public_external_items = all_public_items[1] all_public_external_items_xml_records = {} @@ -1695,14 +1695,15 @@ def perform_request_delete_note(uid, #warnings_notes.append('WRN_WEBBASKET_DELETE_INVALID_NOTE') warnings_html += webbasket_templates.tmpl_warnings(exc.message, ln) - (body, warnings, navtrail) = perform_request_display(uid=uid, - selected_category=category, - selected_topic=topic, - selected_group_id=group_id, - selected_bskid=bskid, - selected_recid=recid, - of='hb', - ln=CFG_SITE_LANG) + (body, dummy, navtrail) = perform_request_display( + uid=uid, + selected_category=category, + selected_topic=topic, + selected_group_id=group_id, + selected_bskid=bskid, + selected_recid=recid, + of='hb', + ln=CFG_SITE_LANG) body = warnings_html + body #warnings.extend(warnings_notes) @@ -2042,7 +2043,7 @@ def perform_request_delete(uid, bskid, confirmed=0, if not(db.check_user_owns_baskets(uid, [bskid])): try: raise InvenioWebBasketWarning(_('Sorry, you do not have sufficient rights on this basket.')) - except InvenioWebBasketWarning, exc: + except InvenioWebBasketWarning: register_exception(stream='warning') #warnings.append(exc.message) #warnings.append(('WRN_WEBBASKET_NO_RIGHTS',)) @@ -2111,7 +2112,7 @@ def perform_request_edit(uid, bskid, topic="", new_name='', if rights != CFG_WEBBASKET_SHARE_LEVELS['MANAGE']: try: raise InvenioWebBasketWarning(_('Sorry, you do not have sufficient rights on this basket.')) - except InvenioWebBasketWarning, exc: + except InvenioWebBasketWarning: register_exception(stream='warning') #warnings.append(exc.message) #warnings.append(('WRN_WEBBASKET_NO_RIGHTS',)) @@ -2470,7 +2471,7 @@ def create_basket_navtrail(uid, if bskid: basket = db.get_public_basket_infos(bskid) if basket: - basket_html = """ &gt; <a class="navtrail" href="%s/yourbaskets/display?%s">""" % \ + basket_html = """ &gt; <a class="navtrail" href="%s/yourbaskets/display?%s">%s</a>""" % \ (CFG_SITE_URL, 'category=' + category + '&amp;ln=' + ln + \ '#bsk' + str(bskid), diff --git a/modules/webbasket/lib/webbasket_dblayer.py b/modules/webbasket/lib/webbasket_dblayer.py index d7fc581a67..546b038158 100644 --- a/modules/webbasket/lib/webbasket_dblayer.py +++ b/modules/webbasket/lib/webbasket_dblayer.py @@ -37,93 +37,6 @@ from invenio.websession_config import CFG_WEBSESSION_USERGROUP_STATUS from invenio.search_engine import get_fieldvalues -########################### Table of contents ################################ -# -# NB. functions preceeded by a star use usergroup table -# -# 1. General functions -# - count_baskets -# - check_user_owns_basket -# - get_max_user_rights_on_basket -# -# 2. Personal baskets -# - get_personal_baskets_info_for_topic -# - get_all_personal_basket_ids_and_names_by_topic -# - get_all_personal_baskets_names -# - get_basket_name -# - is_personal_basket_valid -# - is_topic_valid -# - get_basket_topic -# - get_personal_topics_infos -# - rename_basket -# - rename_topic -# - move_baskets_to_topic -# - delete_basket -# - create_basket -# -# 3. Actions on baskets -# - get_basket_record -# - get_basket_content -# - get_basket_item -# - get_basket_item_title_and_URL -# - share_basket_with_group -# - update_rights -# - move_item -# - delete_item -# - add_to_basket -# - get_external_records_by_collection -# - store_external_records -# - store_external_urls -# - store_external_source -# - get_external_colid_and_url -# -# 4. Group baskets -# - get_group_basket_infos -# - get_group_name -# - get_all_group_basket_ids_and_names_by_group -# - (*) get_all_group_baskets_names -# - is_shared_to -# -# 5. External baskets (baskets user has subscribed to) -# - get_external_baskets_infos -# - get_external_basket_info -# - get_all_external_basket_ids_and_names -# - count_external_baskets -# - get_all_external_baskets_names -# -# 6. Public baskets (interface to subscribe to baskets) -# - get_public_basket_infos -# - get_public_basket_info -# - get_basket_general_infos -# - get_basket_owner_id -# - count_public_baskets -# - get_public_baskets_list -# - is_basket_public -# - subscribe -# - unsubscribe -# - is_user_subscribed_to_basket -# - count_subscribers -# - (*) get_groups_subscribing_to_basket -# - get_rights_on_public_basket -# -# 7. Annotating -# - get_notes -# - get_note -# - save_note -# - delete_note -# - note_belongs_to_item_in_basket_p -# -# 8. Usergroup functions -# - (*) get_group_infos -# - count_groups_user_member_of -# - (*) get_groups_user_member_of -# -# 9. auxilliary functions -# - __wash_sql_count -# - __decompress_last -# - create_pseudo_record -# - prettify_url - ########################## General functions ################################## def count_baskets(uid): @@ -433,7 +346,7 @@ def create_basket(uid, basket_name, topic): def get_all_items_in_user_personal_baskets(uid, topic="", - format='hb'): + of='hb'): """For the specified user, return all the items in their personal baskets, grouped by basket if local or as a list if external. If topic is set, return only that topic's items.""" @@ -441,11 +354,11 @@ def get_all_items_in_user_personal_baskets(uid, if topic: topic_clause = """AND ubsk.topic=%s""" params_local = (uid, uid, topic) - params_external = (uid, uid, topic, format) + params_external = (uid, uid, topic, of) else: topic_clause = "" params_local = (uid, uid) - params_external = (uid, uid, format) + params_external = (uid, uid, of) query_local = """ SELECT rec.id_bskBASKET, @@ -541,53 +454,7 @@ def get_all_user_topics(uid): ########################## Actions on baskets ################################# -def get_basket_record(bskid, recid, format='hb'): - """get record recid in basket bskid - """ - if recid < 0: - rec_table = 'bskEXTREC' - format_table = 'bskEXTFMT' - id_field = 'id_bskEXTREC' - sign = '-' - else: - rec_table = 'bibrec' - format_table = 'bibfmt' - id_field = 'id_bibrec' - sign = '' - query = """ - SELECT DATE_FORMAT(record.creation_date, '%%%%Y-%%%%m-%%%%d %%%%H:%%%%i:%%%%s'), - DATE_FORMAT(record.modification_date, '%%%%Y-%%%%m-%%%%d %%%%H:%%%%i:%%%%s'), - DATE_FORMAT(bskREC.date_added, '%%%%Y-%%%%m-%%%%d %%%%H:%%%%i:%%%%s'), - user.nickname, - count(cmt.id_bibrec_or_bskEXTREC), - DATE_FORMAT(max(cmt.date_creation), '%%%%Y-%%%%m-%%%%d %%%%H:%%%%i:%%%%s'), - fmt.value - - FROM bskREC LEFT JOIN user - ON bskREC.id_user_who_added_item=user.id - LEFT JOIN bskRECORDCOMMENT cmt - ON bskREC.id_bibrec_or_bskEXTREC=cmt.id_bibrec_or_bskEXTREC - LEFT JOIN %(rec_table)s record - ON (%(sign)sbskREC.id_bibrec_or_bskEXTREC=record.id) - LEFT JOIN %(format_table)s fmt - ON (record.id=fmt.%(id_field)s) - - WHERE bskREC.id_bskBASKET=%%s AND - bskREC.id_bibrec_or_bskEXTREC=%%s AND - fmt.format=%%s - - GROUP BY bskREC.id_bibrec_or_bskEXTREC - """ % {'rec_table': rec_table, - 'sign': sign, - 'format_table': format_table, - 'id_field':id_field} - params = (int(bskid), int(recid), format) - res = run_sql(query, params) - if res: - return __decompress_last(res[0]) - return () - -def get_basket_content(bskid, format='hb'): +def get_basket_content(bskid, of='hb'): """Get all records for a given basket.""" query = """ SELECT rec.id_bibrec_or_bskEXTREC, @@ -621,7 +488,7 @@ def get_basket_content(bskid, format='hb'): ORDER BY rec.score""" - params = (format, format, int(bskid)) + params = (of, of, int(bskid)) res = run_sql(query, params) @@ -631,7 +498,7 @@ def get_basket_content(bskid, format='hb'): return res return () -def get_basket_item(bskid, recid, format='hb'): +def get_basket_item(bskid, recid, of='hb'): """Get item (recid) for a given basket.""" query = """ SELECT rec.id_bibrec_or_bskEXTREC, @@ -660,7 +527,7 @@ def get_basket_item(bskid, recid, format='hb'): AND rec.id_bibrec_or_bskEXTREC=%s GROUP BY rec.id_bibrec_or_bskEXTREC ORDER BY rec.score""" - params = (format, format, bskid, recid) + params = (of, of, bskid, recid) res = run_sql(query, params) if res: queryU = """UPDATE bskBASKET SET nb_views=nb_views+1 WHERE id=%s""" @@ -1348,7 +1215,7 @@ def get_basket_share_level(bskid): def get_all_items_in_user_group_baskets(uid, group=0, - format='hb'): + of='hb'): """For the specified user, return all the items in their group baskets, grouped by basket if local or as a list if external. If group is set, return only that group's items.""" @@ -1356,11 +1223,11 @@ def get_all_items_in_user_group_baskets(uid, if group: group_clause = """AND ugbsk.id_usergroup=%s""" params_local = (group, uid) - params_external = (group, uid, format) + params_external = (group, uid, of) else: group_clause = "" params_local = (uid,) - params_external = (uid, format) + params_external = (uid, of) query_local = """ SELECT rec.id_bskBASKET, @@ -1631,7 +1498,7 @@ def get_all_external_baskets_names(uid, return run_sql(query, params) def get_all_items_in_user_public_baskets(uid, - format='hb'): + of='hb'): """For the specified user, return all the items in the public baskets they are subscribed to, grouped by basket if local or as a list if external.""" @@ -1679,7 +1546,7 @@ def get_all_items_in_user_public_baskets(uid, WHERE rec.id_bibrec_or_bskEXTREC < 0 ORDER BY rec.id_bskBASKET""" - params_external = (uid, uid, format) + params_external = (uid, uid, of) res_external = run_sql(query_external, params_external) @@ -1720,7 +1587,7 @@ def get_all_items_in_user_public_baskets_by_matching_notes(uid, return res -def get_all_items_in_all_public_baskets(format='hb'): +def get_all_items_in_all_public_baskets(of='hb'): """Return all the items in all the public baskets, grouped by basket if local or as a list if external.""" @@ -1758,7 +1625,7 @@ def get_all_items_in_all_public_baskets(format='hb'): WHERE rec.id_bibrec_or_bskEXTREC < 0 ORDER BY rec.id_bskBASKET""" - params_external = (format,) + params_external = (of,) res_external = run_sql(query_external, params_external) diff --git a/modules/webbasket/lib/webbasket_templates.py b/modules/webbasket/lib/webbasket_templates.py index 3d82ad8b5f..6e108bd9e1 100644 --- a/modules/webbasket/lib/webbasket_templates.py +++ b/modules/webbasket/lib/webbasket_templates.py @@ -41,9 +41,7 @@ CFG_SITE_RECORD from invenio.webuser import get_user_info from invenio.dateutils import convert_datetext_to_dategui -from invenio.webbasket_dblayer import get_basket_item_title_and_URL, \ - get_basket_ids_and_names -from invenio.bibformat import format_record +from invenio.webbasket_dblayer import get_basket_ids_and_names class Template: """Templating class for webbasket module""" @@ -3026,7 +3024,7 @@ def tmpl_basket_single_item_content(self, </tr>""" % _("The item you have selected does not exist.") else: - (recid, colid, dummy, last_cmt, val, dummy) = item + (recid, colid, dummy, dummy, val, dummy) = item if recid < 0: external_item_img = '<img src="%s/img/wb-external-item.png" alt="%s" style="vertical-align: top;" />&nbsp;' % \ @@ -3889,8 +3887,7 @@ def tmpl_public_basket_single_item_content(self, </tr>""" % {'count': index_item, 'icon': external_item_img, 'content': colid >= 0 and val or val and self.tmpl_create_pseudo_item(val) or _("This record does not seem to exist any more"), - 'notes': notes, - 'ln': ln} + 'notes': notes,} item_html += """ </tbody>""" @@ -4169,9 +4166,9 @@ def tmpl_create_export_as_list(self, recid) export_as_html = "" - for format in list_of_export_as_formats: + for of in list_of_export_as_formats: export_as_html += """<a style="text-decoration:underline;font-weight:normal" href="%s&amp;of=%s">%s</a>, """ % \ - (href, format[1], format[0]) + (href, of[1], of[0]) if export_as_html: export_as_html = export_as_html[:-2] out = """ @@ -4361,13 +4358,13 @@ def create_add_box_select_options(category, if len(personal_basket_list) == 1: bskids = personal_basket_list[0][1].split(',') if len(bskids) == 1: - b = CFG_WEBBASKET_CATEGORIES['PRIVATE'] + '_' + bskids[0] + b = CFG_WEBBASKET_CATEGORIES['PRIVATE'] + '_' + bskids[0] elif len(group_basket_list) == 1: bskids = group_basket_list[0][1].split(',') if len(bskids) == 1: - b = CFG_WEBBASKET_CATEGORIES['GROUP'] + '_' + bskids[0] + b = CFG_WEBBASKET_CATEGORIES['GROUP'] + '_' + bskids[0] - # Create the <optgroup>s and <option>s for the user personal baskets. + # Create the optgroups and options for the user personal baskets. if personal_basket_list: out += """ <optgroup label="%s">""" % ('* ' + _('Your personal baskets') + ' *',) @@ -4390,7 +4387,7 @@ def create_add_box_select_options(category, out += """ </optgroup>""" - # Create the <optgroup>s and <option>s for the user group baskets. + # Create the optgroups and options for the user group baskets. if group_basket_list: out += """ <optgroup label="%s">""" % ('* ' + _('Your group baskets') + ' *',) diff --git a/modules/webbasket/lib/webbasket_webinterface.py b/modules/webbasket/lib/webbasket_webinterface.py index f47c6cf01c..13e5d37f60 100644 --- a/modules/webbasket/lib/webbasket_webinterface.py +++ b/modules/webbasket/lib/webbasket_webinterface.py @@ -409,7 +409,7 @@ def search(self, req, form): p=argd['p'], b=argd['b'], n=argd['n'], -# format=argd['of'], +# record_format=argd['of'], ln=argd['ln']) # register event in webstat diff --git a/modules/webcomment/lib/webcomment.py b/modules/webcomment/lib/webcomment.py index 3dd09d59e7..1db8e44f1d 100644 --- a/modules/webcomment/lib/webcomment.py +++ b/modules/webcomment/lib/webcomment.py @@ -469,21 +469,22 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): params = (cmt_id, uid, client_ip_address, action_date, action_code) run_sql(query, params) if nb_abuse_reports % CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN == 0: - (cmt_id2, + (dummy, id_bibrec, id_user, cmt_body, cmt_date, cmt_star, - cmt_vote, cmt_nb_votes_total, + dummy, + dummy, cmt_title, cmt_reported, - round_name, - restriction) = query_get_comment(cmt_id) + dummy, + dummy) = query_get_comment(cmt_id) (user_nb_abuse_reports, user_votes, user_nb_votes_total) = query_get_user_reports_and_votes(int(id_user)) - (nickname, user_email, last_login) = query_get_user_contact_info(id_user) + (nickname, user_email, dummy) = query_get_user_contact_info(id_user) from_addr = '%s Alert Engine <%s>' % (CFG_SITE_NAME, CFG_WEBALERT_ALERT_ENGINE_EMAIL) comment_collection = get_comment_collection(cmt_id) to_addrs = get_collection_moderators(comment_collection) @@ -508,12 +509,10 @@ def perform_request_report(cmt_id, client_ip_address, uid=-1): ---end body--- Please go to the record page %(comment_admin_link)s to delete this message if necessary. A warning will be sent to the user in question.''' % \ - { 'cfg-report_max' : CFG_WEBCOMMENT_NB_REPORTS_BEFORE_SEND_EMAIL_TO_ADMIN, - 'nickname' : nickname, + { 'nickname' : nickname, 'user_email' : user_email, 'uid' : id_user, 'user_nb_abuse_reports' : user_nb_abuse_reports, - 'user_votes' : user_votes, 'votes' : CFG_WEBCOMMENT_ALLOW_REVIEWS and \ "total number of positive votes\t= %s\n\t\ttotal number of negative votes\t= %s" % \ (user_votes, (user_nb_votes_total - user_votes)) or "\n", @@ -874,7 +873,6 @@ def query_add_comment_or_remark(reviews=0, recID=0, uid=-1, msg="", #change general unicode back to utf-8 msg = msg.encode('utf-8') note = note.encode('utf-8') - msg_original = msg (restriction, round_name) = get_record_status(recID) if attached_files is None: attached_files = {} @@ -1116,7 +1114,7 @@ def get_users_subscribed_to_discussion(recID, check_authorizations=True): uid = row[0] if check_authorizations: user_info = collect_user_info(uid) - (auth_code, auth_msg) = check_user_can_view_comments(user_info, recID) + (auth_code, dummy) = check_user_can_view_comments(user_info, recID) else: # Don't check and grant access auth_code = False @@ -1562,7 +1560,6 @@ def perform_request_add_comment_or_remark(recID=0, if warnings is None: warnings = [] - actions = ['DISPLAY', 'REPLY', 'SUBMIT'] _ = gettext_set_language(ln) ## check arguments @@ -1757,18 +1754,18 @@ def notify_admin_of_new_comment(comID): id_user, body, date_creation, - star_score, nb_votes_yes, nb_votes_total, + star_score, dummy, dummy, title, - nb_abuse_reports, round_name, restriction) = comment + dummy, dummy, dummy) = comment else: return user_info = query_get_user_contact_info(id_user) if len(user_info) > 0: - (nickname, email, last_login) = user_info + (nickname, email, dummy) = user_info if not len(nickname) > 0: nickname = email.split('@')[0] else: - nickname = email = last_login = "ERROR: Could not retrieve" + nickname = email = "ERROR: Could not retrieve" review_stuff = ''' Star score = %s diff --git a/modules/webmessage/lib/webmessage.py b/modules/webmessage/lib/webmessage.py index 1e5cd9f2f6..4462463e52 100644 --- a/modules/webmessage/lib/webmessage.py +++ b/modules/webmessage/lib/webmessage.py @@ -191,7 +191,7 @@ def perform_request_write(uid, @type ln: string @return: body with warnings. """ - warnings = [] + body = "" _ = gettext_set_language(ln) msg_from_nickname = "" diff --git a/modules/websearch/lib/websearch_yoursearches.py b/modules/websearch/lib/websearch_yoursearches.py index 605588187b..7a475c8681 100644 --- a/modules/websearch/lib/websearch_yoursearches.py +++ b/modules/websearch/lib/websearch_yoursearches.py @@ -19,10 +19,9 @@ __revision__ = "$Id$" -from invenio.config import CFG_SITE_LANG, CFG_SITE_SECURE_URL +from invenio.config import CFG_SITE_LANG from invenio.dbquery import run_sql from invenio.messages import gettext_set_language -from invenio.webuser import isGuestUser from urllib import quote from invenio.webalert import count_user_alerts_for_given_query diff --git a/modules/websession/lib/webaccount.py b/modules/websession/lib/webaccount.py index bc4fca0403..2f8d373428 100644 --- a/modules/websession/lib/webaccount.py +++ b/modules/websession/lib/webaccount.py @@ -29,10 +29,8 @@ CFG_SITE_LANG, \ CFG_SITE_SUPPORT_EMAIL, \ CFG_SITE_ADMIN_EMAIL, \ - CFG_SITE_SECURE_URL, \ CFG_VERSION, \ CFG_SITE_RECORD -from invenio.access_control_engine import acc_authorize_action from invenio.access_control_config import CFG_EXTERNAL_AUTHENTICATION, \ SUPERADMINROLE, CFG_EXTERNAL_AUTH_DEFAULT from invenio.dbquery import run_sql @@ -65,9 +63,7 @@ def perform_info(req, ln): return websession_templates.tmpl_account_info( ln = ln, uid = uid, - guest = int(user_info['guest']), - CFG_CERN_SITE = CFG_CERN_SITE, - ) + guest = int(user_info['guest'])) def perform_display_external_user_settings(settings, ln): """show external user settings which is a dictionary.""" @@ -235,7 +231,7 @@ def superuser_account_warnings(): # no account nick-named `admin' exists; keep on going res1 = [] - for user in res1: + for dummy in res1: warning_array.append("warning_empty_admin_password") #Check if the admin email has been changed from the default diff --git a/modules/websession/lib/webgroup.py b/modules/websession/lib/webgroup.py index 99a2e780b6..ff638bd22b 100644 --- a/modules/websession/lib/webgroup.py +++ b/modules/websession/lib/webgroup.py @@ -781,7 +781,6 @@ def account_user_groups(uid, ln=CFG_SITE_LANG): out = websession_templates.tmpl_account_user_groups( nb_admin_groups, - nb_member_groups, nb_total_groups, ln = ln) diff --git a/modules/websession/lib/websession_templates.py b/modules/websession/lib/websession_templates.py index cf09bb43f2..649229ec1c 100644 --- a/modules/websession/lib/websession_templates.py +++ b/modules/websession/lib/websession_templates.py @@ -69,7 +69,6 @@ def tmpl_back_form(self, ln, message, url, link): 'message' : message, 'url' : url, 'link' : link, - 'ln' : ln } return out @@ -315,7 +314,6 @@ def tmpl_user_preferences(self, ln, email, email_disabled, password_disabled, ni </form> """ % { 'change_pass' : _("If you want to change your password, please enter the old one and set the new value in the form below."), - 'mandatory' : _("mandatory"), 'old_password' : _("Old password"), 'new_password' : _("New password"), 'csrf_token': cgi.escape(csrf_token, True), @@ -562,7 +560,7 @@ def tmpl_lost_password_form(self, ln): return out - def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): + def tmpl_account_info(self, ln, uid, guest): """ Displays the account information @@ -573,8 +571,6 @@ def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): - 'uid' *string* - The user id - 'guest' *boolean* - If the user is guest - - - 'CFG_CERN_SITE' *boolean* - If the site is a CERN site """ # load the right message language @@ -645,7 +641,7 @@ def tmpl_account_info(self, ln, uid, guest, CFG_CERN_SITE): return out - def tmpl_warning_guest_user(self, ln, type): + def tmpl_warning_guest_user(self, warning_type, ln): """ Displays a warning message about the specified type @@ -653,16 +649,16 @@ def tmpl_warning_guest_user(self, ln, type): - 'ln' *string* - The language to display the interface in - - 'type' *string* - The type of data that will get lost in case of guest account (for the moment: 'alerts' or 'baskets') + - 'warning_type' *string* - The type of data that will get lost in case of guest account (for the moment: 'alerts' or 'baskets') """ # load the right message language _ = gettext_set_language(ln) - if (type=='baskets'): + if (warning_type == 'baskets'): msg = _("You are logged in as a guest user, so your baskets will disappear at the end of the current session.") + ' ' - elif (type=='alerts'): + elif (warning_type == 'alerts'): msg = _("You are logged in as a guest user, so your alerts will disappear at the end of the current session.") + ' ' - elif (type=='searches'): + elif (warning_type == 'searches'): msg = _("You are logged in as a guest user, so your searches will disappear at the end of the current session.") + ' ' msg += _("If you wish you can %(x_url_open)slogin or register here%(x_url_close)s.") % {'x_url_open': '<a href="' + CFG_SITE_SECURE_URL + '/youraccount/login?ln=' + ln + '">', 'x_url_close': '</a>'} @@ -1822,7 +1818,7 @@ def tmpl_display_member_groups(self, groups, ln=CFG_SITE_LANG): </td> </tr>""" %(_("You are not a member of any groups."),) for group_data in groups: - (id, name, description) = group_data + (dummy, name, description) = group_data group_text += """ <tr class="mailboxrecord"> <td>%s</td> @@ -1885,7 +1881,7 @@ def tmpl_display_external_groups(self, groups, ln=CFG_SITE_LANG): </td> </tr>""" %(_("You are not a member of any external groups."),) for group_data in groups: - (id, name, description) = group_data + (dummy, name, description) = group_data group_text += """ <tr class="mailboxrecord"> <td>%s</td> @@ -2645,13 +2641,11 @@ def tmpl_delete_msg(self, def tmpl_account_user_groups( self, nb_admin_groups = 0, - nb_member_groups = 0, nb_total_groups = 0, ln = CFG_SITE_LANG): """ Information on the user's groups for the "Your Account" page @param nb_admin_groups: number of groups the user is admin of - @param nb_member_groups: number of groups the user is member of @param nb_total_groups: number of groups the user belongs to @param ln: language return: html output. diff --git a/modules/websession/lib/websession_webinterface.py b/modules/websession/lib/websession_webinterface.py index 73599cb9c1..ca02481616 100644 --- a/modules/websession/lib/websession_webinterface.py +++ b/modules/websession/lib/websession_webinterface.py @@ -26,7 +26,6 @@ import cgi from datetime import timedelta -import os import re from invenio.config import \ @@ -38,7 +37,6 @@ CFG_SITE_SUPPORT_EMAIL, \ CFG_SITE_SECURE_URL, \ CFG_SITE_URL, \ - CFG_CERN_SITE, \ CFG_WEBSESSION_RESET_PASSWORD_EXPIRE_IN_DAYS, \ CFG_OPENAIRE_SITE from invenio import webuser @@ -61,7 +59,7 @@ InvenioWebAccessMailCookieDeletedError, mail_cookie_check_authorize_action from invenio.access_control_config import CFG_WEBACCESS_WARNING_MSGS, \ CFG_EXTERNAL_AUTH_USING_SSO, CFG_EXTERNAL_AUTH_LOGOUT_SSO, \ - CFG_EXTERNAL_AUTHENTICATION, CFG_EXTERNAL_AUTH_SSO_REFRESH, \ + CFG_EXTERNAL_AUTHENTICATION, \ CFG_OPENID_CONFIGURATIONS, CFG_OAUTH2_CONFIGURATIONS, \ CFG_OAUTH1_CONFIGURATIONS, CFG_OAUTH2_PROVIDERS, CFG_OAUTH1_PROVIDERS, \ CFG_OPENID_PROVIDERS, CFG_OPENID_AUTHENTICATION, \ @@ -130,7 +128,7 @@ def access(self, req, form): body += "<p>" + _("You can now go to %(x_url_open)syour account page%(x_url_close)s.") % {'x_url_open' : '<a href="/youraccount/display?ln=%s">' % args['ln'], 'x_url_close' : '</a>'} + "</p>" return page(title=_("Email address successfully activated"), body=body, req=req, language=args['ln'], uid=webuser.getUid(req), lastupdated=__lastupdated__, navmenuid='youraccount', secure_page_p=1) - except InvenioWebAccessMailCookieDeletedError, e: + except InvenioWebAccessMailCookieDeletedError: body = "<p>" + _("You have already confirmed the validity of your email address!") + "</p>" if CFG_ACCESS_CONTROL_LEVEL_ACCOUNTS == 1: body += "<p>" + _("Please, wait for the administrator to " @@ -910,7 +908,7 @@ def login(self, req, form): if args['action']: cookie = args['action'] try: - action, arguments = mail_cookie_check_authorize_action(cookie) + dummy, dummy = mail_cookie_check_authorize_action(cookie) except InvenioWebAccessMailCookieError: pass if not CFG_EXTERNAL_AUTH_USING_SSO: From 072d95927ff18c7fb2285377d2b3d2a9a873c17f Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 27 Mar 2014 11:24:20 +0100 Subject: [PATCH 69/83] Miscutil: fix get_authenticated_mechanize_browser * Fixes the string checked in get_authenticated_mechanize_browser to make sure that the user was logged in correctly. --- modules/miscutil/lib/testutils.py | 2 +- modules/websession/lib/websession_templates.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/miscutil/lib/testutils.py b/modules/miscutil/lib/testutils.py index f1773104c2..2c3ea644d6 100644 --- a/modules/miscutil/lib/testutils.py +++ b/modules/miscutil/lib/testutils.py @@ -223,7 +223,7 @@ def get_authenticated_mechanize_browser(username="guest", password=""): browser.submit() username_account_page_body = browser.response().read() try: - username_account_page_body.index("You are logged in as %s." % username) + username_account_page_body.index("You are logged in as %s." % ("<strong>" + cgi.escape(username) + "</strong>"),) except ValueError: raise InvenioTestUtilsBrowserException('ERROR: Cannot login as %s.' % username) return browser diff --git a/modules/websession/lib/websession_templates.py b/modules/websession/lib/websession_templates.py index 649229ec1c..f05030ade1 100644 --- a/modules/websession/lib/websession_templates.py +++ b/modules/websession/lib/websession_templates.py @@ -808,13 +808,17 @@ def tmpl_account_page( # While there are still items to display, get the first item, # removing it from the list of items to display. item = items.pop(0) - # The following "1-liner" does the following (>= Python 2.5): + # The following "one-liner" does the following (>= Python 2.5): # * For each of the 3 lists (1 for each column) # calculate the sum of the length of its items (see the lambda). - # The lenght of an itme is the literal length of the string - # to be displayed, which includes HTML code. This of course is - # not the most accurate way, but a good approximation and also + # The lenght of an item is the literal length of the string + # to be displayed, which includes HTML code. This, of course, is + # not the most accurate way, but it is a good approximation and also # very efficient. + # [A more accurate way would be to wash the HTML tags first + # (HTMLParser/re/...) and then compare the string sizes. But we should + # still account for line breaks, paragraphs, list items and other HTML + # tags that introduce white space.] # * Pick the list that has the smallest sum. # * Append the current item to that list. min(items_in_column_1_of_3, From 0c98c82f8c2951e0a05a2ffa945329b6af0e9764 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Thu, 27 Mar 2014 19:21:12 +0100 Subject: [PATCH 70/83] Websearch: remove mentions of total searches * Removes mentions of the number of total searches the user has performed in favor of only the unique searches to avoid possible confusion. --- modules/websearch/lib/websearch_templates.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/modules/websearch/lib/websearch_templates.py b/modules/websearch/lib/websearch_templates.py index 087dbdab8e..8a82447e61 100644 --- a/modules/websearch/lib/websearch_templates.py +++ b/modules/websearch/lib/websearch_templates.py @@ -5148,14 +5148,12 @@ def tmpl_yoursearches_display(self, # Diplay a message about the number of searches. if p: - msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches including the term %(p)s.") % \ + msg = _("You have performed %(searches_distinct)s unique searches including the term %(p)s.") % \ {'searches_distinct': '<strong>' + str(nb_queries_distinct) + '</strong>', - 'searches_total': '<strong>' + str(nb_queries_total) + '</strong>', 'p': '<strong>' + cgi.escape(p) + '</strong>'} else: - msg = _("You have performed %(searches_distinct)s unique searches in a total of %(searches_total)s searches.") % \ - {'searches_distinct': '<strong>' + str(nb_queries_distinct) + '</strong>', - 'searches_total': '<strong>' + str(nb_queries_total) + '</strong>'} + msg = _("You have performed %(searches_distinct)s unique searches.") % \ + {'searches_distinct': '<strong>' + str(nb_queries_distinct) + '</strong>',} out = '<p>' + msg + '</p>' # Search form @@ -5263,11 +5261,10 @@ def tmpl_account_user_searches(self, unique, total, ln = CFG_SITE_LANG): _ = gettext_set_language(ln) if unique > 0: - out = _("You have performed %(x_url_open)s%(unique)s unique searches%(x_url_close)s in a total of %(total)s searches.") % \ + out = _("You have performed %(x_url_open)s%(x_unique)s unique searches%(x_url_close)s.") % \ {'x_url_open' : '<strong><a href="%s/yoursearches/display?ln=%s">' % (CFG_SITE_SECURE_URL, ln), - 'unique' : str(unique), - 'x_url_close' : '</a></strong>', - 'total' : str(total),} + 'x_unique' : str(unique), + 'x_url_close' : '</a></strong>',} else: out = _("You have not searched for anything yet. You may want to start by the %(x_url_open)ssearch interface%(x_url_close)s first.") % \ {'x_url_open' : '<a href="%s/?ln=%s">' % (CFG_SITE_SECURE_URL, ln), From 77f94ed421322978fa1440493ac8e86fe0d9613d Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Fri, 28 Mar 2014 11:21:42 +0100 Subject: [PATCH 71/83] General: fix failing tests --- modules/webhelp/web/hacking/test-suite.webdoc | 2 +- modules/websession/lib/webgroup_regression_tests.py | 4 ++-- modules/websession/lib/webuser_regression_tests.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/webhelp/web/hacking/test-suite.webdoc b/modules/webhelp/web/hacking/test-suite.webdoc index ec94fac788..282d8174ac 100644 --- a/modules/webhelp/web/hacking/test-suite.webdoc +++ b/modules/webhelp/web/hacking/test-suite.webdoc @@ -382,7 +382,7 @@ browser.submit() username_account_page_body = browser.response().read() try: string.index(username_account_page_body, - "You are logged in as userfoo.") + "You are logged in as &lt;strong&gt;userfoo&lt;/strong&gt;.") except ValueError: self.fail('ERROR: Cannot login as userfoo.') </pre> diff --git a/modules/websession/lib/webgroup_regression_tests.py b/modules/websession/lib/webgroup_regression_tests.py index 5bb431bf92..38360b1924 100644 --- a/modules/websession/lib/webgroup_regression_tests.py +++ b/modules/websession/lib/webgroup_regression_tests.py @@ -116,7 +116,7 @@ def test_external_groups_visibility_groupspage(self): browser['p_pw'] = '' browser.submit() - expected_response = "You are logged in as admin" + expected_response = "You are logged in as <strong>admin</strong>." login_response_body = browser.response().read() try: login_response_body.index(expected_response) @@ -151,7 +151,7 @@ def test_external_groups_visibility_messagespage(self): browser['p_pw'] = '' browser.submit() - expected_response = "You are logged in as admin" + expected_response = "You are logged in as <strong>admin</strong>." login_response_body = browser.response().read() try: login_response_body.index(expected_response) diff --git a/modules/websession/lib/webuser_regression_tests.py b/modules/websession/lib/webuser_regression_tests.py index 03d8189522..6bc70c7ebb 100644 --- a/modules/websession/lib/webuser_regression_tests.py +++ b/modules/websession/lib/webuser_regression_tests.py @@ -63,7 +63,7 @@ def test_password_setting(self): browser['p_pw'] = '' browser.submit() - expected_response = "You are logged in as admin" + expected_response = "You are logged in as <strong>admin</strong>." login_response_body = browser.response().read() try: login_response_body.index(expected_response) @@ -196,7 +196,7 @@ def test_select_records_per_group(self): browser['p_pw'] = '' browser.submit() - expected_response = "You are logged in as admin" + expected_response = "You are logged in as <strong>admin</strong>." login_response_body = browser.response().read() try: login_response_body.index(expected_response) @@ -261,7 +261,7 @@ def test_select_records_per_group(self): browser['p_pw'] = '' browser.submit() - expected_response = "You are logged in as admin" + expected_response = "You are logged in as <strong>admin</strong>." login_response_body = browser.response().read() try: login_response_body.index(expected_response) From e60fda7e01532b2d87293b273c3f4a2a69430cfa Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Tue, 2 Jun 2015 11:03:15 +0200 Subject: [PATCH 72/83] BibFormat: correct MARC field usage in bfe_doi * BETTER Corrects subfield usage when displaying the DOI qualifying information, changing subfield `y` (not valid) by `q`. Signed-off-by: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> --- modules/bibformat/lib/elements/bfe_doi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bibformat/lib/elements/bfe_doi.py b/modules/bibformat/lib/elements/bfe_doi.py index dce48f2315..6c3bbb226d 100644 --- a/modules/bibformat/lib/elements/bfe_doi.py +++ b/modules/bibformat/lib/elements/bfe_doi.py @@ -39,10 +39,10 @@ def format_element(bfo, tag="909C4", label="", separator="<br/> ", description_l break if not fields: fields = bfo.fields(tag) - doi_list = [] + doi_list = [] for field in fields: if field.get('2', 'DOI') == 'DOI' and 'a' in field: - desc = field.get('y', '') + desc = field.get('y', '') or field.get('q', '') front = end = '' if desc: if description_location == 'front': From e91de3fad2322aa93c7f469402c8364f825f21da Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Fri, 17 Jul 2015 16:59:40 +0200 Subject: [PATCH 73/83] WebStyle: wsgi handler IP parser enhancement * BETTER Cleans the port from the user's IP preventing issues in future manipulations of it. Reviewed-by: Samuele Kaplun <samuele.kaplun@cern.ch> Signed-off-by: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> --- modules/webstyle/lib/webinterface_handler_wsgi.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/webstyle/lib/webinterface_handler_wsgi.py b/modules/webstyle/lib/webinterface_handler_wsgi.py index 99bd19d65f..a163446c95 100644 --- a/modules/webstyle/lib/webinterface_handler_wsgi.py +++ b/modules/webstyle/lib/webinterface_handler_wsgi.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # This file is part of Invenio. -# Copyright (C) 2009, 2010, 2011, 2012 CERN. +# Copyright (C) 2009, 2010, 2011, 2012, 2015 CERN. # # Invenio is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -61,10 +61,9 @@ # any src usage of an external website _RE_HTTPS_REPLACES = re.compile(r"\b((?:src\s*=|url\s*\()\s*[\"']?)http\://", re.I) -# Regexp to verify that the IP starts with a number (filter cases where 'unknown') -# It is faster to verify only the start (585 ns) compared with verifying -# the whole ip address - re.compile('^\d+\.\d+\.\d+\.\d+$') (1.01 µs) -_RE_IPADDRESS_START = re.compile(r"^\d+\.") +# Regexp to verify the IP (filter cases where 'unknown'). +_RE_IPADDRESS = re.compile(r"^\d+(\.\d+){3}$") +_RE_IPADDRESS_WITH_PORT = re.compile(r"^\d+(\.\d+){3}:\d+$") # Regexp to match IE User-Agent _RE_BAD_MSIE = re.compile(r"MSIE\s+(\d+\.\d+)") @@ -275,8 +274,11 @@ def get_remote_ip(self): # we trust this proxy ip_list = self.__headers_in['X-FORWARDED-FOR'].split(',') for ip in ip_list: - if _RE_IPADDRESS_START.match(ip): + if _RE_IPADDRESS.match(ip): return ip + # Probably because behind a proxy + elif _RE_IPADDRESS_WITH_PORT.match(ip): + return ip[:ip.index(':')] # no IP has the correct format, return a default IP return '10.0.0.10' else: From 43abf0b0d37968413e88dd10473e4dc4ccb5e1c4 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Fri, 17 Jul 2015 17:01:05 +0200 Subject: [PATCH 74/83] WebStyle: kwalitee fix * Improves `webinterface_handler_wsgi` PEP8 code style. Signed-off-by: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> --- .../webstyle/lib/webinterface_handler_wsgi.py | 219 ++++++++++-------- 1 file changed, 119 insertions(+), 100 deletions(-) diff --git a/modules/webstyle/lib/webinterface_handler_wsgi.py b/modules/webstyle/lib/webinterface_handler_wsgi.py index a163446c95..7b1103ea28 100644 --- a/modules/webstyle/lib/webinterface_handler_wsgi.py +++ b/modules/webstyle/lib/webinterface_handler_wsgi.py @@ -16,39 +16,50 @@ # along with Invenio; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -"""mod_python->WSGI Framework""" +"""mod_python->WSGI Framework.""" -import sys -import os -import re import cgi import gc import inspect +import os +import re import socket +import sys + from fnmatch import fnmatch from urlparse import urlparse, urlunparse - -from wsgiref.validate import validator from wsgiref.util import FileWrapper +from wsgiref.validate import validator if __name__ != "__main__": # Chances are that we are inside mod_wsgi. - ## You can't write to stdout in mod_wsgi, but some of our - ## dependecies do this! (e.g. 4Suite) + # You can't write to stdout in mod_wsgi, but some of our + # dependecies do this! (e.g. 4Suite) sys.stdout = sys.stderr -from invenio.urlutils import redirect_to_url +from invenio.config import ( + CFG_DEVEL_SITE, + CFG_SITE_LANG, + CFG_SITE_SECURE_URL, + CFG_SITE_URL, + CFG_WEBDIR, + CFG_WEBSTYLE_HTTP_STATUS_ALERT_LIST, + CFG_WEBSTYLE_REVERSE_PROXY_IPS +) +from invenio.errorlib import get_pretty_traceback, register_exception from invenio.session import get_session -from invenio.webinterface_handler import CFG_HAS_HTTPS_SUPPORT, CFG_FULL_HTTPS +from invenio.urlutils import redirect_to_url +from invenio.webinterface_handler import CFG_FULL_HTTPS, CFG_HAS_HTTPS_SUPPORT +from invenio.webinterface_handler_config import ( + DONE, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_NOT_FOUND, + HTTP_STATUS_MAP, + OK, + SERVER_RETURN +) +from invenio.webinterface_handler_wsgi_utils import FieldStorage, table from invenio.webinterface_layout import invenio_handler -from invenio.webinterface_handler_wsgi_utils import table, FieldStorage -from invenio.webinterface_handler_config import \ - HTTP_STATUS_MAP, SERVER_RETURN, OK, DONE, \ - HTTP_NOT_FOUND, HTTP_INTERNAL_SERVER_ERROR -from invenio.config import CFG_WEBDIR, CFG_SITE_LANG, \ - CFG_WEBSTYLE_HTTP_STATUS_ALERT_LIST, CFG_DEVEL_SITE, CFG_SITE_URL, \ - CFG_SITE_SECURE_URL, CFG_WEBSTYLE_REVERSE_PROXY_IPS -from invenio.errorlib import register_exception, get_pretty_traceback # Static files are usually handled directly by the webserver (e.g. Apache) # However in case WSGI is required to handle static files too (such @@ -70,31 +81,41 @@ def _http_replace_func(match): - ## src external_site -> CFG_SITE_SECURE_URL/sslredirect/external_site + # src external_site -> CFG_SITE_SECURE_URL/sslredirect/external_site return match.group(1) + CFG_SITE_SECURE_URL + '/sslredirect/' _ESCAPED_CFG_SITE_URL = cgi.escape(CFG_SITE_URL, True) _ESCAPED_CFG_SITE_SECURE_URL = cgi.escape(CFG_SITE_SECURE_URL, True) + + def https_replace(html): html = html.replace(_ESCAPED_CFG_SITE_URL, _ESCAPED_CFG_SITE_SECURE_URL) return _RE_HTTPS_REPLACES.sub(_http_replace_func, html) + class InputProcessed(object): + """ Auxiliary class used when reading input. + @see: <http://www.wsgi.org/wsgi/Specifications/handling_post_forms>. """ + def read(self, *args): raise EOFError('The wsgi.input stream has already been consumed') readline = readlines = __iter__ = read + class SimulatedModPythonRequest(object): + """ mod_python like request object. + Minimum and cleaned implementation to make moving out of mod_python easy. @see: <http://www.modpython.org/live/current/doc-html/pyapi-mprequest.html> """ + def __init__(self, environ, start_response): self.__environ = environ self.__start_response = start_response @@ -110,7 +131,7 @@ def __init__(self, environ, start_response): self.__allowed_methods = [] self.__cleanups = [] self.headers_out = self.__headers - ## See: <http://www.python.org/dev/peps/pep-0333/#the-write-callable> + # See: <http://www.python.org/dev/peps/pep-0333/#the-write-callable> self.__write = None self.__write_error = False self.__errors = environ['wsgi.errors'] @@ -121,7 +142,7 @@ def __init__(self, environ, start_response): self.track_writings = False self.__what_was_written = "" self.__cookies_out = {} - self.g = {} ## global dictionary in case it's needed + self.g = {} # global dictionary in case it's needed for key, value in environ.iteritems(): if key.startswith('HTTP_'): self.__headers_in[key[len('HTTP_'):].replace('_', '-')] = value @@ -131,14 +152,12 @@ def __init__(self, environ, start_response): self.__headers_in['content-type'] = environ['CONTENT_TYPE'] def set_cookie(self, cookie): - """ - This function is used to cumulate identical cookies. - """ + """This function is used to cumulate identical cookies.""" self.__cookies_out[cookie.name] = cookie def _write_cookies(self): if self.__cookies_out: - if not self.headers_out.has_key("Set-Cookie"): + if "Set-Cookie" not in self.headers_out: g = _RE_BAD_MSIE.search(self.headers_in.get('User-Agent', "MSIE 6.0")) bad_msie = g and float(g.group(1)) < 9.0 if not (bad_msie and self.is_https()): @@ -153,13 +172,13 @@ def get_post_form(self): self.__tainted = True post_form = self.__environ.get('wsgi.post_form') input = self.__environ['wsgi.input'] - if (post_form is not None - and post_form[0] is input): + if (post_form is not None and + post_form[0] is input): return post_form[2] # This must be done to avoid a bug in cgi.FieldStorage self.__environ.setdefault('QUERY_STRING', '') - ## Video handler hack: + # Video handler hack: uri = self.__environ['PATH_INFO'] if uri.endswith("upload_video"): tmp_shared = True @@ -210,10 +229,11 @@ def flush(self): self.__what_was_written += self.__buffer except IOError, err: if "failed to write data" in str(err) or "client connection closed" in str(err): - ## Let's just log this exception without alerting the admin: + # Let's just log this exception without alerting the admin: register_exception(req=self) - self.__write_error = True ## This flag is there just - ## to not report later other errors to the admin. + self.__write_error = True + # This flag is there just + # to not report later other errors to the admin. else: raise self.__buffer = '' @@ -234,7 +254,7 @@ def send_http_header(self): if self.__allowed_methods and self.__status.startswith('405 ') or self.__status.startswith('501 '): self.__headers['Allow'] = ', '.join(self.__allowed_methods) - ## See: <http://www.python.org/dev/peps/pep-0333/#the-write-callable> + # See: <http://www.python.org/dev/peps/pep-0333/#the-write-callable> #print self.__low_level_headers self.__write = self.__start_response(self.__status, self.__low_level_headers) self.__response_sent_p = True @@ -266,9 +286,9 @@ def get_args(self): def get_remote_ip(self): if 'X-FORWARDED-FOR' in self.__headers_in and \ - self.__headers_in.get('X-FORWARDED-SERVER', '') == \ - self.__headers_in.get('X-FORWARDED-HOST', '') == \ - urlparse(CFG_SITE_URL)[1]: + self.__headers_in.get('X-FORWARDED-SERVER', '') == \ + self.__headers_in.get('X-FORWARDED-HOST', '') == \ + urlparse(CFG_SITE_URL)[1]: # we are using proxy setup if self.__environ.get('REMOTE_ADDR') in CFG_WEBSTYLE_REVERSE_PROXY_IPS: # we trust this proxy @@ -283,11 +303,12 @@ def get_remote_ip(self): return '10.0.0.10' else: # we don't trust this proxy - register_exception(prefix="You are running in a proxy configuration, but the " + \ - "CFG_WEBSTYLE_REVERSE_PROXY_IPS variable does not contain " + \ - "the IP of your proxy, thus the remote IP addresses of your " + \ - "clients are not trusted. Please configure this variable.", - alert_admin=True) + register_exception( + prefix="You are running in a proxy configuration, but the " + "CFG_WEBSTYLE_REVERSE_PROXY_IPS variable does not contain " + "the IP of your proxy, thus the remote IP addresses of your " + "clients are not trusted. Please configure this variable.", + alert_admin=True) return '10.0.0.11' return self.__environ.get('REMOTE_ADDR') @@ -338,7 +359,7 @@ def sendfile(self, path, offset=0, the_len=-1): raise except IOError, err: if "failed to write data" in str(err) or "client connection closed" in str(err): - ## Let's just log this exception without alerting the admin: + # Let's just log this exception without alerting the admin: register_exception(req=self) else: raise @@ -392,10 +413,10 @@ def readline(self, hint=None): try: return self.__environ['wsgi.input'].readline(hint) except TypeError: - ## the hint param is not part of wsgi pep, although - ## it's great to exploit it in when reading FORM - ## with large files, in order to avoid filling up the memory - ## Too bad it's not there :-( + # the hint param is not part of wsgi pep, although + # it's great to exploit it in when reading FORM + # with large files, in order to avoid filling up the memory + # Too bad it's not there :-( return self.__environ['wsgi.input'].readline() def readlines(self, hint=None): @@ -428,9 +449,8 @@ def __str__(self): return out def get_original_wsgi_environment(self): - """ - Return the original WSGI environment used to initialize this request - object. + """Return the original WSGI environment used to initialize this object. + @return: environ, start_response @raise AssertionError: in case the environment has been altered, i.e. either the input has been consumed or something has already been @@ -466,9 +486,10 @@ def get_environ(self): referer = property(get_referer) what_was_written = property(get_what_was_written) + def alert_admin_for_server_status_p(status, referer): - """ - Check the configuration variable + """Check the configuration variable. + CFG_WEBSTYLE_HTTP_STATUS_ALERT_LIST to see if the exception should be registered and the admin should be alerted. """ @@ -477,20 +498,19 @@ def alert_admin_for_server_status_p(status, referer): pattern = pattern.lower() must_have_referer = False if pattern.endswith('r'): - ## e.g. "404 r" + # e.g. "404 r" must_have_referer = True - pattern = pattern[:-1].strip() ## -> "404" + pattern = pattern[:-1].strip() # -> "404" if fnmatch(status, pattern) and (not must_have_referer or referer): return True return False + def application(environ, start_response): - """ - Entry point for wsgi. - """ - ## Needed for mod_wsgi, see: <http://code.google.com/p/modwsgi/wiki/ApplicationIssues> + """Entry point for wsgi.""" + # Needed for mod_wsgi, see: <http://code.google.com/p/modwsgi/wiki/ApplicationIssues> req = SimulatedModPythonRequest(environ, start_response) - #print 'Starting mod_python simulation' + # print 'Starting mod_python simulation' try: try: if (CFG_FULL_HTTPS or (CFG_HAS_HTTPS_SUPPORT and get_session(req).need_https)) and not req.is_https(): @@ -503,7 +523,7 @@ def application(environ, start_response): # Compute the new path plain_path = original_parts[2] plain_path = secure_prefix_parts[2] + \ - plain_path[len(plain_prefix_parts[2]):] + plain_path[len(plain_prefix_parts[2]):] # ...and recompose the complete URL final_parts = list(secure_prefix_parts) @@ -531,8 +551,8 @@ def application(environ, start_response): if status not in (OK, DONE): req.status = status req.headers_out['content-type'] = 'text/html' - admin_to_be_alerted = alert_admin_for_server_status_p(status, - req.headers_in.get('referer')) + admin_to_be_alerted = alert_admin_for_server_status_p( + status, req.headers_in.get('referer')) if admin_to_be_alerted: register_exception(req=req, alert_admin=True) if not req.response_sent_p: @@ -555,12 +575,12 @@ def application(environ, start_response): return generate_error_page(req, page_already_started=True) finally: try: - ## Let's save the session. + # Let's save the session. session = get_session(req) try: if req.is_https() or not session.need_https: - ## We save the session only if it's safe to do it, i.e. - ## if we well had a valid session. + # We save the session only if it's safe to do it, i.e. + # if we well had a valid session. session.dirty = True session.save() if 'user_info' in req._session: @@ -568,35 +588,31 @@ def application(environ, start_response): finally: del session except Exception: - ## What could have gone wrong? + # What could have gone wrong? register_exception(req=req, alert_admin=True) if hasattr(req, '_session'): - ## The session handler saves for caching a request_wrapper - ## in req. - ## This saves req as an attribute, creating a circular - ## reference. - ## Since we have have reached the end of the request handler - ## we can safely drop the request_wrapper so to avoid - ## memory leaks. + # The session handler saves for caching a request_wrapper in req. + # This saves req as an attribute, creating a circular reference. + # Since we have have reached the end of the request handler + # we can safely drop the request_wrapper so to avoid memory leaks. delattr(req, '_session') if hasattr(req, '_user_info'): - ## For the same reason we can delete the user_info. + # For the same reason we can delete the user_info. delattr(req, '_user_info') for (callback, data) in req.get_cleanups(): callback(data) - ## as suggested in - ## <http://www.python.org/doc/2.3.5/lib/module-gc.html> + # as suggested in + # <http://www.python.org/doc/2.3.5/lib/module-gc.html> gc.enable() gc.collect() del gc.garbage[:] return [] + def generate_error_page(req, admin_was_alerted=True, page_already_started=False): - """ - Returns an iterable with the error page to be sent to the user browser. - """ + """Return an iterable with the error page to be sent to the browser.""" from invenio.webpage import page from invenio import template webstyle_templates = template.load('webstyle') @@ -606,9 +622,11 @@ def generate_error_page(req, admin_was_alerted=True, page_already_started=False) else: return [page(title=req.get_wsgi_status(), body=webstyle_templates.tmpl_error_page(status=req.get_wsgi_status(), ln=ln, admin_was_alerted=admin_was_alerted), language=ln, req=req)] + def is_static_path(path): """ - Returns True if path corresponds to an exsting file under CFG_WEBDIR. + Return True if path corresponds to an exsting file under CFG_WEBDIR. + @param path: the path. @type path: string @return: True if path corresponds to an exsting file under CFG_WEBDIR. @@ -619,9 +637,10 @@ def is_static_path(path): return path return None + def is_mp_legacy_publisher_path(path): - """ - Checks path corresponds to an exsting Python file under CFG_WEBDIR. + """Check path corresponds to an exsting Python file under CFG_WEBDIR. + @param path: the path. @type path: string @return: the path of the module to load and the function to call there. @@ -641,36 +660,35 @@ def is_mp_legacy_publisher_path(path): else: return None, None + def mp_legacy_publisher(req, possible_module, possible_handler): - """ - mod_python legacy publisher minimum implementation. - """ + """mod_python legacy publisher minimum implementation.""" the_module = open(possible_module).read() module_globals = {} exec(the_module, module_globals) if possible_handler in module_globals and callable(module_globals[possible_handler]): from invenio.webinterface_handler import _check_result - ## req is the required first parameter of any handler + # req is the required first parameter of any handler expected_args = list(inspect.getargspec(module_globals[possible_handler])[0]) if not expected_args or 'req' != expected_args[0]: - ## req was not the first argument. Too bad! + # req was not the first argument. Too bad! raise SERVER_RETURN, HTTP_NOT_FOUND - ## the req.form must be casted to dict because of Python 2.4 and earlier - ## otherwise any object exposing the mapping interface can be - ## used with the magic ** + # the req.form must be casted to dict because of Python 2.4 and earlier + # otherwise any object exposing the mapping interface can be + # used with the magic ** form = dict(req.form) for key, value in form.items(): - ## FIXME: this is a backward compatibility workaround - ## because most of the old administration web handler - ## expect parameters to be of type str. - ## When legacy publisher will be removed all this - ## pain will go away anyway :-) + # FIXME: this is a backward compatibility workaround + # because most of the old administration web handler + # expect parameters to be of type str. + # When legacy publisher will be removed all this + # pain will go away anyway :-) if isinstance(value, str): form[key] = str(value) else: - ## NOTE: this is a workaround for e.g. legacy webupload - ## that is still using legacy publisher and expect to - ## have a file (Field) instance instead of a string. + # NOTE: this is a workaround for e.g. legacy webupload + # that is still using legacy publisher and expect to + # have a file (Field) instance instead of a string. form[key] = value if (CFG_FULL_HTTPS or CFG_HAS_HTTPS_SUPPORT and get_session(req).need_https) and not req.is_https(): @@ -684,7 +702,7 @@ def mp_legacy_publisher(req, possible_module, possible_handler): # Compute the new path plain_path = original_parts[2] plain_path = secure_prefix_parts[2] + \ - plain_path[len(plain_prefix_parts[2]):] + plain_path[len(plain_prefix_parts[2]):] # ...and recompose the complete URL final_parts = list(secure_prefix_parts) @@ -718,6 +736,7 @@ def mp_legacy_publisher(req, possible_module, possible_handler): else: raise SERVER_RETURN, HTTP_NOT_FOUND + def check_wsgiref_testing_feasability(): """ In order to use wsgiref for running Invenio, CFG_SITE_URL and @@ -736,10 +755,9 @@ def check_wsgiref_testing_feasability(): Currently CFG_SITE_SECURE_URL is set to: "%s".""" % CFG_SITE_SECURE_URL sys.exit(1) + def wsgi_handler_test(port=80): - """ - Simple WSGI testing environment based on wsgiref. - """ + """Simple WSGI testing environment based on wsgiref.""" from wsgiref.simple_server import make_server global CFG_WSGI_SERVE_STATIC_FILES CFG_WSGI_SERVE_STATIC_FILES = True @@ -749,6 +767,7 @@ def wsgi_handler_test(port=80): print "Serving on port %s..." % port httpd.serve_forever() + def main(): from optparse import OptionParser parser = OptionParser() From 3f0b6e67ebd23808e94d312ec3840f5fa0d942ef Mon Sep 17 00:00:00 2001 From: Harris Tzovanakis <me@drjova.com> Date: Tue, 23 Jun 2015 10:51:12 +0200 Subject: [PATCH 75/83] WebSearch: tabs miss language parameter * FIX Adds the proper language parameter on tabs if the user has selected different language from the browser preferred. (closes #308) Signed-off-by: Harris Tzovanakis <me@drjova.com> --- modules/websearch/lib/search_engine.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/websearch/lib/search_engine.py b/modules/websearch/lib/search_engine.py index ab15175be8..32431dadab 100644 --- a/modules/websearch/lib/search_engine.py +++ b/modules/websearch/lib/search_engine.py @@ -4677,8 +4677,10 @@ def print_records(req, recIDs, jrec=1, rg=CFG_WEBSEARCH_DEF_RECORDS_IN_GROUPS, f link_ln = '' - if ln != CFG_SITE_LANG: - link_ln = '?ln=%s' % ln + if 'ln' in req.form: + link_ln = "?ln={0}".format( + req.form.get('ln', CFG_SITE_LANG) + ) recid_to_display = recid # Record ID used to build the URL. if CFG_WEBSEARCH_USE_ALEPH_SYSNOS: From e1fe846e01aaf623de6a604be6d300185317739b Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Fri, 24 Jul 2015 11:17:18 +0200 Subject: [PATCH 76/83] WebSubmit: author autocomplition name order * Fixes the order of `firstname` and `lastname` in `typeahead` to be consistent with `LDAP`. (closes #cds-655) * Closes #127 by cherry-pick Signed-off-by: Harris Tzovanakis <me@drjova.com> --- modules/websubmit/lib/websubmit_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/websubmit/lib/websubmit_templates.py b/modules/websubmit/lib/websubmit_templates.py index cf018b6631..a2b73d3c3d 100644 --- a/modules/websubmit/lib/websubmit_templates.py +++ b/modules/websubmit/lib/websubmit_templates.py @@ -3154,7 +3154,7 @@ def tmpl_authors_autocompletion( } function dispkey(suggestion_object) { - var name = (suggestion_object["firstname"] === undefined) ? suggestion_object["name"] : suggestion_object["firstname"] + ", " + suggestion_object["lastname"]; + var name = (suggestion_object["firstname"] === undefined) ? suggestion_object["name"] : suggestion_object["lastname"] + ", " + suggestion_object["firstname"]; var affiliation = (suggestion_object["affiliation"] === undefined) ? "" : ": " + suggestion_object["affiliation"]; return name + affiliation; } From 1934c246fe08d811697f895c91eb3562d7984a8b Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Tue, 14 Jul 2015 17:04:50 +0200 Subject: [PATCH 77/83] WebSubmit: subtitle file addition to converter --- modules/websubmit/lib/websubmit_file_converter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/websubmit/lib/websubmit_file_converter.py b/modules/websubmit/lib/websubmit_file_converter.py index 75255e62b3..25a52d12bc 100644 --- a/modules/websubmit/lib/websubmit_file_converter.py +++ b/modules/websubmit/lib/websubmit_file_converter.py @@ -138,6 +138,8 @@ def get_conversion_map(): '.hocr': {}, '.pdf;pdfa': {}, '.asc': {}, + '.vtt': {}, + '.srt': {}, } if CFG_PATH_GZIP: ret['.ps']['.ps.gz'] = (gzip, {}) @@ -186,6 +188,9 @@ def get_conversion_map(): ret['.html']['.txt'] = (html2text, {}) ret['.htm']['.txt'] = (html2text, {}) ret['.xml']['.txt'] = (html2text, {}) + # Subtitle files + ret['.vtt']['.txt'] = (txt2text, {}) + ret['.srt']['.txt'] = (txt2text, {}) if CFG_PATH_TIFF2PDF: ret['.tiff']['.pdf'] = (tiff2pdf, {}) ret['.tif']['.pdf'] = (tiff2pdf, {}) From 35e268dba932d7cedec3532eb31ee31bd0e0f180 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Tue, 14 Jul 2015 17:05:05 +0200 Subject: [PATCH 78/83] BibIndex: external file index enhancement Signed-off-by: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> --- config/invenio.conf | 2 ++ .../tokenizers/BibIndexFulltextTokenizer.py | 36 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/config/invenio.conf b/config/invenio.conf index 408411ba6a..bca923df90 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -1171,6 +1171,8 @@ CFG_BIBINDEX_PERFORM_OCR_ON_DOCNAMES = scan-.* # NOTE: for backward compatibility reasons you can set this to a simple # regular expression that will directly be used as the unique key of the # map, with corresponding value set to ".*" (in order to match any URL) +# NOTE2: If the value is None, the url mapping the key regex will be used +# directly CFG_BIBINDEX_SPLASH_PAGES = { "http://documents\.cern\.ch/setlink\?.*": ".*", "http://ilcagenda\.linearcollider\.org/subContributionDisplay\.py\?.*|http://ilcagenda\.linearcollider\.org/contributionDisplay\.py\?.*": "http://ilcagenda\.linearcollider\.org/getFile\.py/access\?.*|http://ilcagenda\.linearcollider\.org/materialDisplay\.py\?.*", diff --git a/modules/bibindex/lib/tokenizers/BibIndexFulltextTokenizer.py b/modules/bibindex/lib/tokenizers/BibIndexFulltextTokenizer.py index c26dbdfbcb..3f2b04a582 100644 --- a/modules/bibindex/lib/tokenizers/BibIndexFulltextTokenizer.py +++ b/modules/bibindex/lib/tokenizers/BibIndexFulltextTokenizer.py @@ -43,6 +43,7 @@ from invenio.bibtask import write_message from invenio.errorlib import register_exception from invenio.intbitset import intbitset +from invenio.search_engine import search_pattern from invenio.bibindex_tokenizers.BibIndexDefaultTokenizer import BibIndexDefaultTokenizer @@ -131,16 +132,18 @@ def get_words_from_fulltext(self, url_direct_or_indirect): for splash_re, url_re in CFG_BIBINDEX_SPLASH_PAGES.iteritems(): if re.match(splash_re, url_direct_or_indirect): write_message("... %s is a splash page (%s)" % (url_direct_or_indirect, splash_re), verbose=2) - html = urllib2.urlopen(url_direct_or_indirect).read() - urls = get_links_in_html_page(html) - write_message("... found these URLs in %s splash page: %s" % (url_direct_or_indirect, ", ".join(urls)), verbose=3) - for url in urls: - if re.match(url_re, url): - write_message("... will index %s (matched by %s)" % (url, url_re), verbose=2) - urls_to_index.add(url) - if not urls_to_index: - urls_to_index.add(url_direct_or_indirect) - write_message("... will extract words from %s" % ', '.join(urls_to_index), verbose=2) + if url_re is None: + urls_to_index.add(url_direct_or_indirect) + continue + else: + html = urllib2.urlopen(url_direct_or_indirect).read() + urls = get_links_in_html_page(html) + write_message("... found these URLs in %s splash page: %s" % (url_direct_or_indirect, ", ".join(urls)), verbose=3) + for url in urls: + if re.match(url_re, url): + write_message("... will index %s (matched by %s)" % (url, url_re), verbose=2) + urls_to_index.add(url) + write_message("... will extract words from {0}".format(urls_to_index), verbose=2) words = {} for url in urls_to_index: tmpdoc = download_url(url) @@ -156,11 +159,14 @@ def get_words_from_fulltext(self, url_direct_or_indirect): indexer = get_idx_indexer('fulltext') if indexer != 'native': - if indexer == 'SOLR' and CFG_SOLR_URL: - solr_add_fulltext(None, text) # FIXME: use real record ID - if indexer == 'XAPIAN' and CFG_XAPIAN_ENABLED: - #xapian_add(None, 'fulltext', text) # FIXME: use real record ID - pass + recids = search_pattern(p='8567_u:{0}'.format(url)) + write_message('... will add words to record {0}'.format(recid), verbose=2) + for recid in recids: + if indexer == 'SOLR' and CFG_SOLR_URL: + solr_add_fulltext(recid, text) + if indexer == 'XAPIAN' and CFG_XAPIAN_ENABLED: + #xapian_add(recid, 'fulltext', text) + pass # we are relying on an external information retrieval system # to provide full-text indexing, so dispatch text to it and # return nothing here: From 9d97516c4318de6e718dabd0f6085bb8cabbb334 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Wed, 15 Jul 2015 13:37:46 +0200 Subject: [PATCH 79/83] BibRank: allow ranking external files using solr Signed-off-by: Esteban J. G. Gabancho <esteban.gabancho@gmail.com> --- .../miscutil/lib/solrutils_bibrank_indexer.py | 81 ++++++++++++++++--- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/modules/miscutil/lib/solrutils_bibrank_indexer.py b/modules/miscutil/lib/solrutils_bibrank_indexer.py index fd0f83da19..06b45cb03d 100644 --- a/modules/miscutil/lib/solrutils_bibrank_indexer.py +++ b/modules/miscutil/lib/solrutils_bibrank_indexer.py @@ -22,17 +22,32 @@ """ +import os +import urllib2 +import re import time -from invenio.config import CFG_SOLR_URL + +from invenio.config import ( + CFG_SOLR_URL, + CFG_BIBINDEX_FULLTEXT_INDEX_LOCAL_FILES_ONLY, + CFG_BIBINDEX_SPLASH_PAGES +) from invenio.bibtask import write_message, task_get_option, task_update_progress, \ task_sleep_now_if_required +from invenio.htmlutils import get_links_in_html_page +from invenio.websubmit_file_converter import convert_file from invenio.dbquery import run_sql -from invenio.search_engine import record_exists -from invenio.bibdocfile import BibRecDocs +from invenio.search_engine import record_exists, get_field_tags +from invenio.search_engine_utils import get_fieldvalues +from invenio.bibdocfile import BibRecDocs, bibdocfile_url_p, download_url from invenio.solrutils_bibindex_indexer import replace_invalid_solr_characters from invenio.bibindex_engine import create_range_list from invenio.errorlib import register_exception from invenio.bibrank_bridge_utils import get_tags, get_field_content_in_utf8 +from invenio.bibtask import write_message + + +SOLR_CONNECTION = None if CFG_SOLR_URL: @@ -103,16 +118,11 @@ def solr_add_range(lower_recid, upper_recid, tags_to_index, next_commit_counter) """ for recid in range(lower_recid, upper_recid + 1): if record_exists(recid): - abstract = get_field_content_in_utf8(recid, 'abstract', tags_to_index) - author = get_field_content_in_utf8(recid, 'author', tags_to_index) - keyword = get_field_content_in_utf8(recid, 'keyword', tags_to_index) - title = get_field_content_in_utf8(recid, 'title', tags_to_index) - try: - bibrecdocs = BibRecDocs(recid) - fulltext = unicode(bibrecdocs.get_text(), 'utf-8') - except: - fulltext = '' - + abstract = get_field_content_in_utf8(recid, 'abstract', tags_to_index) + author = get_field_content_in_utf8(recid, 'author', tags_to_index) + keyword = get_field_content_in_utf8(recid, 'keyword', tags_to_index) + title = get_field_content_in_utf8(recid, 'title', tags_to_index) + fulltext = _get_fulltext(recid) solr_add(recid, abstract, author, fulltext, keyword, title) next_commit_counter = solr_commit_if_necessary(next_commit_counter,recid=recid) @@ -182,3 +192,48 @@ def word_index(run): # pylint: disable=W0613 write_message("No new records. Solr index is up to date") write_message("Solr ranking indexer completed") + + +def _get_fulltext(recid): + + words = [] + try: + bibrecdocs = BibRecDocs(recid) + words.append(unicode(bibrecdocs.get_text(), 'utf-8')) + except Exception, e: + pass + + if CFG_BIBINDEX_FULLTEXT_INDEX_LOCAL_FILES_ONLY: + write_message("... %s is external URL but indexing only local files" % url, verbose=2) + return ' '.join(words) + + urls_from_record = [url for tag in get_field_tags('fulltext') + for url in get_fieldvalues(recid, tag) + if not bibdocfile_url_p(url)] + urls_to_index = set() + for url_direct_or_indirect in urls_from_record: + for splash_re, url_re in CFG_BIBINDEX_SPLASH_PAGES.iteritems(): + if re.match(splash_re, url_direct_or_indirect): + if url_re is None: + write_message("... %s is file to index (%s)" % (url_direct_or_indirect, splash_re), verbose=2) + urls_to_index.add(url_direct_or_indirect) + continue + write_message("... %s is a splash page (%s)" % (url_direct_or_indirect, splash_re), verbose=2) + html = urllib2.urlopen(url_direct_or_indirect).read() + urls = get_links_in_html_page(html) + write_message("... found these URLs in %s splash page: %s" % (url_direct_or_indirect, ", ".join(urls)), verbose=3) + for url in urls: + if re.match(url_re, url): + write_message("... will index %s (matched by %s)" % (url, url_re), verbose=2) + urls_to_index.add(url) + if urls_to_index: + write_message("... will extract words from %s:%s" % (recid, ', '.join(urls_to_index)), verbose=2) + for url in urls_to_index: + tmpdoc = download_url(url) + try: + tmptext = convert_file(tmpdoc, output_format='.txt') + words.append(open(tmptext).read()) + os.remove(tmptext) + finally: + os.remove(tmpdoc) + return ' '.join(words) From 3af3710d9cbf10d7ff1d1266aa538cf05f8bbf23 Mon Sep 17 00:00:00 2001 From: Flavio Costa <flavio.costa@cern.ch> Date: Fri, 17 Jul 2015 14:11:43 +0200 Subject: [PATCH 80/83] WebJournal: navigation menu category replacement * Changes in main navigation menu: Training->Learning --- .../lib/elements/bfe_webjournal_main_navigation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/webjournal/lib/elements/bfe_webjournal_main_navigation.py b/modules/webjournal/lib/elements/bfe_webjournal_main_navigation.py index 50341f2617..158d623031 100644 --- a/modules/webjournal/lib/elements/bfe_webjournal_main_navigation.py +++ b/modules/webjournal/lib/elements/bfe_webjournal_main_navigation.py @@ -20,7 +20,7 @@ """ WebJournal element - Prints main (horizontal) navigation menu """ -from invenio.webjournal_utils import \ +from invenio.legacy.webjournal_utils import \ parse_url_string, \ make_journal_url, \ get_journal_categories @@ -64,7 +64,7 @@ def format_element(bfo, category_prefix, category_suffix, separator=" | ", linkattrd = {'class':'selectedNavigationPage'} if journal_name == 'CERNBulletin' and \ category == 'Training and Development': - category = 'Training' + category = 'Learning' if ln == 'fr': category = 'Formations' category_link = create_html_link(category_url, {}, @@ -94,6 +94,6 @@ def escape_values(bfo): dummy = _("Training and Development") dummy = _("General Information") dummy = _("Announcements") -dummy = _("Training") +dummy = _("Learning") dummy = _("Events") dummy = _("Staff Association") From 1ee4b23d0fceed670ddbefd6fce299aeae56c8d5 Mon Sep 17 00:00:00 2001 From: Nikolaos Kasioumis <nikolaos.kasioumis@cern.ch> Date: Fri, 21 Dec 2012 16:18:36 +0100 Subject: [PATCH 81/83] WebNews: New module to display and manage news * The WebNews module that will be responsible for presenting the site's news to the users and keeping a history of them. * This first release includes the tooltip functionality and the database structure. (addresses #1288) --- configure.ac | 4 + modules/Makefile.am | 1 + ...nvenio_2013_02_15_webnews_new_db_tables.py | 82 +++++ modules/webnews/Makefile.am | 20 ++ modules/webnews/doc/Makefile.am | 18 ++ modules/webnews/lib/Makefile.am | 36 +++ modules/webnews/lib/webnews.css | 162 ++++++++++ modules/webnews/lib/webnews.js | 256 +++++++++++++++ modules/webnews/lib/webnews.py | 291 ++++++++++++++++++ modules/webnews/lib/webnews_config.py | 34 ++ modules/webnews/lib/webnews_dblayer.py | 99 ++++++ modules/webnews/lib/webnews_utils.py | 62 ++++ modules/webnews/lib/webnews_webinterface.py | 81 +++++ modules/webnews/web/Makefile.am | 18 ++ modules/webstyle/lib/webinterface_layout.py | 10 +- modules/webstyle/lib/webstyle_templates.py | 28 ++ 16 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 modules/miscutil/lib/upgrades/invenio_2013_02_15_webnews_new_db_tables.py create mode 100644 modules/webnews/Makefile.am create mode 100644 modules/webnews/doc/Makefile.am create mode 100644 modules/webnews/lib/Makefile.am create mode 100644 modules/webnews/lib/webnews.css create mode 100644 modules/webnews/lib/webnews.js create mode 100644 modules/webnews/lib/webnews.py create mode 100644 modules/webnews/lib/webnews_config.py create mode 100644 modules/webnews/lib/webnews_dblayer.py create mode 100644 modules/webnews/lib/webnews_utils.py create mode 100644 modules/webnews/lib/webnews_webinterface.py create mode 100644 modules/webnews/web/Makefile.am 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 = '<div id="' + id + '" class="ttn">\n'; + tooltip_html += ' <div class="ttn_text">' + body + '</div>\n'; + tooltip_html += ' <div class="ttn_actions">\n'; + // TODO: Do not add the "Read more" label until the /news interface is ready. + //tooltip_html += ' <a class="ttn_actions_read_more" href="' + readmore_url + '">' + readmore + '</a>\n'; + //tooltip_html += ' &nbsp;|&nbsp;\n'; + tooltip_html += ' <a class="ttn_actions_dismiss" href="#">' + dismiss + '</a>\n'; + tooltip_html += ' </div>\n'; + tooltip_html += ' <div class="ttn_arrow"></div>\n'; + tooltip_html += ' <div class="ttn_arrow_border"></div>\n'; + tooltip_html += '</div>\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="", <meta name="keywords" content="%(keywords)s" /> <script type="text/javascript" src="%(cssurl)s/js/jquery.min.js"></script> %(hepDataAdditions)s + <!-- WebNews CSS library --> + <link rel="stylesheet" href="%(cssurl)s/img/webnews.css" type="text/css" /> + <!-- WebNews JS library --> + <script type="text/javascript" src="%(cssurl)s/js/webnews.js"></script> %(metaheaderadd)s </head> <body%(body_css_classes)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 = """ +<script> +// WebNews tooltips +$(document).ready(function() { + $.ajax({ + url: "/news/tooltips", + success: function(data) { + create_tooltips(data); + }, + dataType: "json", + cache: false + }); +}); +</script> +""" + else: + tooltips_script = "" + out = """ <div class="pagefooter"> %(pagefooteradd)s @@ -564,6 +589,7 @@ def tmpl_pagefooter(self, req=None, ln=CFG_SITE_LANG, lastupdated=None, </div> <!-- replaced page footer --> </div> +%(tooltips_script)s </body> </html> """ % { @@ -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 From be2fef6f6174c7a075a5874d7a17a300584873f9 Mon Sep 17 00:00:00 2001 From: "Esteban J. G. Gabancho" <esteban.gabancho@gmail.com> Date: Fri, 24 Jul 2015 12:22:35 +0200 Subject: [PATCH 82/83] Fix requirements.txt typo * Merge pull request #89 drjova/fix-requirement. * NOTE This problem should be addressed in invenio#2431. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a37d64b373..8741f54fc3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ redis==2.9.0 nydus==0.10.6 Cerberus==0.5 matplotlib -git+https://github.com/CERNDocumentServer/lumberjack.git html2text==2014.7.3 markdown2==2.2.1 bleach==1.4 +git+git://github.com/CERNDocumentServer/lumberjack.git From 746cf8a3324922df8729e00b60e57c505f2d0175 Mon Sep 17 00:00:00 2001 From: Sebastian Witowski <witowski.sebastian@gmail.com> Date: Fri, 8 Jan 2016 12:58:03 +0100 Subject: [PATCH 83/83] BibConvert: modify oaiharvest xsl template * Adjusts the template to new CERN report numbering schema. Signed-off-by: Sebastian Witowski <witowski.sebastian@gmail.com> --- modules/bibconvert/etc/oaiarxiv2marcxml.xsl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/bibconvert/etc/oaiarxiv2marcxml.xsl b/modules/bibconvert/etc/oaiarxiv2marcxml.xsl index 8a52f58fdc..95122a74c8 100644 --- a/modules/bibconvert/etc/oaiarxiv2marcxml.xsl +++ b/modules/bibconvert/etc/oaiarxiv2marcxml.xsl @@ -610,10 +610,10 @@ </datafield> </xsl:if> - <xsl:if test="contains(./OAI-PMH:metadata/arXiv:arXiv/arXiv:report-no, 'CERN-PH-EP')"> + <xsl:if test="contains(./OAI-PMH:metadata/arXiv:arXiv/arXiv:report-no, 'CERN-EP')"> <datafield tag="084" ind1 = " " ind2 = " "> <subfield code="a"> - <xsl:variable name="reportdate" select="substring-after($RN3, 'CERN-PH-EP-')"/>PH-EP-<xsl:choose> + <xsl:variable name="reportdate" select="substring-after($RN3, 'CERN-EP-')"/>EP-<xsl:choose> <xsl:when test="contains($reportdate,',')"> <xsl:value-of select="substring-before($reportdate, ',')"/> </xsl:when> @@ -625,7 +625,7 @@ <subfield code="2">CERN Library</subfield> </datafield> <datafield tag="710" ind1 = " " ind2 = " "> - <subfield code="5">PH-EP</subfield> + <subfield code="5">EP</subfield> </datafield> </xsl:if> @@ -771,7 +771,7 @@ <xsl:if test="./OAI-PMH:metadata/arXiv:arXiv/arXiv:license"> <datafield tag="540" ind1=" " ind2=" "> <subfield code="u"><xsl:value-of select="normalize-space(./OAI-PMH:metadata/arXiv:arXiv/arXiv:license)"/></subfield> - <subfield code="b">arXiv</subfield> + <subfield code="b">arXiv</subfield> <xsl:choose> <xsl:when test="contains(./OAI-PMH:metadata/arXiv:arXiv/arXiv:license, 'creativecommons.org/licenses/by/3.0')"> <subfield code="a">CC-BY-3.0</subfield>