diff --git a/.kwalitee.yml b/.kwalitee.yml index a5051794d3..97829ce6b4 100644 --- a/.kwalitee.yml +++ b/.kwalitee.yml @@ -47,6 +47,7 @@ components: - BibSword - DocExtract - ElmSubmit +- I18N - OAIHarvest - OAIRepositoy - PdfChecker diff --git a/INSTALL b/INSTALL index 4ed9b4e700..e584746094 100644 --- a/INSTALL +++ b/INSTALL @@ -5,7 +5,7 @@ About ===== This document specifies how to build, customize, and install Invenio -v1.2.0 for the first time. See RELEASE-NOTES if you are upgrading +v1.2.1 for the first time. See RELEASE-NOTES if you are upgrading from a previous Invenio release. Contents @@ -83,6 +83,9 @@ Contents natively in UTF-8 mode by setting "default-character-set=utf8" in various parts of your "my.cnf" file, such as in the "[mysql]" part and elsewhere; but this is not really required. + Note also that you may encounter problems when MySQL is run in + "strict mode"; you may want to configure your "my.cnf" in order + to avoid using strict mode (such as `STRICT_ALL_TABLES`). c) Redis server (may be on a remote machine) for user session @@ -301,13 +304,13 @@ Contents ---------------- $ cd $HOME/src/ - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz.md5 - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz.sig - $ md5sum -c invenio-1.2.0.tar.gz.md5 - $ gpg --verify invenio-1.2.0.tar.gz.sig invenio-1.2.0.tar.gz - $ tar xvfz invenio-1.2.0.tar.gz - $ cd invenio-1.2.0 + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz.md5 + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz.sig + $ md5sum -c invenio-1.2.1.tar.gz.md5 + $ gpg --verify invenio-1.2.1.tar.gz.sig invenio-1.2.1.tar.gz + $ tar xvfz invenio-1.2.1.tar.gz + $ cd invenio-1.2.1 $ ./configure $ make $ make install @@ -355,19 +358,19 @@ Contents sources. (The built files will be installed into different "target" directories later.) - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz.md5 - $ wget http://invenio-software.org/download/invenio-1.2.0.tar.gz.sig + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz.md5 + $ wget http://invenio-software.org/download/invenio-1.2.1.tar.gz.sig Fetch Invenio source tarball from the distribution server, together with MD5 checksum and GnuPG cryptographic signature files useful for verifying the integrity of the tarball. - $ md5sum -c invenio-1.2.0.tar.gz.md5 + $ md5sum -c invenio-1.2.1.tar.gz.md5 Verify MD5 checksum. - $ gpg --verify invenio-1.2.0.tar.gz.sig invenio-1.2.0.tar.gz + $ gpg --verify invenio-1.2.1.tar.gz.sig invenio-1.2.1.tar.gz Verify GnuPG cryptographic signature. Note that you may first have to import my public key into your keyring, if you @@ -379,11 +382,11 @@ Contents warning that may follow after the signature has been successfully verified. - $ tar xvfz invenio-1.2.0.tar.gz + $ tar xvfz invenio-1.2.1.tar.gz Untar the distribution tarball. - $ cd invenio-1.2.0 + $ cd invenio-1.2.1 Go to the source directory. diff --git a/Makefile.am b/Makefile.am index 6f44617c95..e0556ef2f7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -30,7 +30,7 @@ MJV = 2.3 MATHJAX = http://invenio-software.org/download/mathjax/MathJax-v$(MJV).zip # current CKeditor version -CKV = 3.6.6 +CKV = 4.5.3 CKEDITOR = ckeditor_$(CKV).zip # current MediaElement.js version @@ -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 && \ @@ -324,6 +332,62 @@ uninstall-pdfa-helper-files: @echo "** The PDF/A helper files were successfully uninstalled. **" @echo "***********************************************************" +<<<<<<< HEAD +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 "***********************************************************" + +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/NEWS b/NEWS index 65782857b8..1b726649b2 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,75 @@ releases. For more information about the current release, please consult RELEASE-NOTES. For more information about changes, please consult ChangeLog. +Invenio v1.2.1 -- released 2015-05-21 +------------------------------------- + +Security fixes +~~~~~~~~~~~~~~ + ++ BibAuthorID: + + - Improves URL redirecting by properly quoting all URL parts, in + order to better protect against possible XSS attacks. + ++ WebStyle: + + - Adds back the `HttpOnly` cookie attribute in order to better + protect against potential XSS vulnerabilities. (#3064) + +Improved features +~~~~~~~~~~~~~~~~~ + ++ installation: + + - Apache virtual environments are now created with appropriate + `WSGIDaemonProcess` user value, taken from the configuration + variable `CFG_BIBSCHED_PROCESS_USER`, provided it is set. This + change makes it easier to run Invenio under non-Apache user + identity. + + - Apache virtual environments are now created with appropriate + `WSGIPythonHome` directive so that it would be easier to run + Invenio from within Python virtual environments. + +Bug fixes +~~~~~~~~~ + ++ BibDocFile: + + - Safer upgrade recipe for migrations from the old document storage + model (used in v1.1) to the new document storage model (used in + v1.2). + ++ WebSearch: + + - Removes special behaviour of the "subject" index that was hard- + coded based on the index name. Installations should rather + specify wanted behaviour by means of configurable tokeniser + instead. + + - Collection names containing slashes are now supported again. + However we recommend not to use slashes in collection names; if + slashes were wanted for aesthetic reasons, they can be added in + visible collection translations. (#2902) + ++ global: + + - Replaces `invenio-demo.cern.ch` by `demo.invenio-software.org` + which is the new canonical URL of the demo site. (#2867) + ++ installation: + + - Releases constraint on using an old version of `h5py` that was + anyway no longer available on PyPI. + ++ testutils: + + - Switches off SSL verification when running the test suite. Useful + for Python-2.7.9 where self-signed SSL certificates (that are + usually used on development installations) would cause apparent + test failures. (#2868) + Invenio v1.1.6 -- released 2015-05-21 ------------------------------------- diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 93cc7042f4..2fd074b30f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,8 +1,8 @@ ============================ - Invenio v1.1.6 is released + Invenio v1.2.1 is released ============================ -Invenio v1.1.6 was released on May 21, 2015. +Invenio v1.2.1 was released on May 21, 2015. About ----- @@ -10,12 +10,17 @@ About Invenio is a digital library framework enabling you to build your own digital library or document repository on the web. -This old stable release update is recommended to all Invenio sites -using v1.1.5 or previous releases. +This stable release update is recommended to all Invenio sites using +v1.2.0 or previous releases. Security fixes -------------- ++ BibAuthorID: + + - Improves URL redirecting by properly quoting all URL parts, in + order to better protect against possible XSS attacks. + + WebStyle: - Adds back the `HttpOnly` cookie attribute in order to better @@ -39,11 +44,34 @@ Improved features Bug fixes --------- ++ BibDocFile: + + - Safer upgrade recipe for migrations from the old document storage + model (used in v1.1) to the new document storage model (used in + v1.2). + ++ WebSearch: + + - Removes special behaviour of the "subject" index that was hard- + coded based on the index name. Installations should rather + specify wanted behaviour by means of configurable tokeniser + instead. + + - Collection names containing slashes are now supported again. + However we recommend not to use slashes in collection names; if + slashes were wanted for aesthetic reasons, they can be added in + visible collection translations. (#2902) + + global: - Replaces `invenio-demo.cern.ch` by `demo.invenio-software.org` which is the new canonical URL of the demo site. (#2867) ++ installation: + + - Releases constraint on using an old version of `h5py` that was + anyway no longer available on PyPI. + + testutils: - Switches off SSL verification when running the test suite. Useful @@ -54,9 +82,9 @@ Bug fixes Download -------- -- http://invenio-software.org/download/invenio-1.1.6.tar.gz -- http://invenio-software.org/download/invenio-1.1.6.tar.gz.md5 -- http://invenio-software.org/download/invenio-1.1.6.tar.gz.sig +- http://invenio-software.org/download/invenio-1.2.1.tar.gz +- http://invenio-software.org/download/invenio-1.2.1.tar.gz.md5 +- http://invenio-software.org/download/invenio-1.2.1.tar.gz.sig Installation ------------ @@ -72,8 +100,8 @@ a) Stop your bibsched queue and your Apache server. b) Install the update:: - $ tar xvfz invenio-1.1.6.tar.gz - $ cd invenio-1.1.6 + $ tar xvfz invenio-1.2.1.tar.gz + $ cd invenio-1.2.1 $ sudo rsync -a /opt/invenio/etc/ /opt/invenio/etc.OLD/ $ sh /opt/invenio/etc/build/config.nice $ make @@ -86,9 +114,11 @@ b) Install the update:: $ sudo -u www-data /opt/invenio/bin/inveniocfg --upgrade (1) If you are upgrading from previous stable release series - (v0.99 or v1.0), please don't run this rsync command but - diff, in order to inspect changes and adapt your old - configuration to the new Invenio v1.1 release series. + (v0.99, v1.0 or v1.1), please don't run this rsync command + but diff, in order to inspect changes and adapt your old + configuration to the new Invenio v1.2 release series. For + more information you may also want to consult release notes + coming with Invenio v1.2.0. c) Restart your Apache server and your bibsched queue. diff --git a/THANKS b/THANKS index edfdc24979..1335b512ac 100644 --- a/THANKS +++ b/THANKS @@ -90,6 +90,12 @@ Several people contributed language translations: - Mehdi Zahedi Contributions to the Persian (Farsi) translation. + - Guillaume Dorsival + Contributions to the French translation. + + - Charlotte Iris Cattaneo + Contributions to the German, Italian, and Spanish translations. + The URL handler was inspired by the Quixote Web Framework which is ``Copyright (c) 2004 Corporation for National Research Initiatives; All Rights Reserved''. diff --git a/config/invenio.conf b/config/invenio.conf index 187f42cbf6..bac764f427 100644 --- a/config/invenio.conf +++ b/config/invenio.conf @@ -940,6 +940,8 @@ CFG_BIBDOCFILE_DOCUMENT_FILE_MANAGER_RESTRICTIONS = [ CFG_BIBDOCFILE_DOCUMENT_FILE_MANAGER_MISC = { 'can_revise_doctypes': ['*'], 'can_comment_doctypes': ['*'], + 'can_change_copyright_doctypes': ['*'], + 'can_change_advanced_copyright_doctypes': ['*'], 'can_describe_doctypes': ['*'], 'can_delete_doctypes': ['*'], 'can_keep_doctypes': ['*'], @@ -1182,6 +1184,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\?.*", @@ -1486,6 +1490,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 ## ################################## @@ -2576,6 +2589,72 @@ 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 + +## 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'] + +############################## +# Recommender Configuration ## +############################## +# CFG_RECOMMENDER_REDIS -- optionally, enables the recommendations and +# specifies the Redis host from where the recommendations are loaded. +# To show the recommendations on the record page include the +# BibFormat element `bfe_record_recommendations`. +CFG_RECOMMENDER_REDIS = + +# CFG_RECOMMENDER_PREFIX -- optionally, defines the prefix used in the +# redis cache. +CFG_RECOMMENDER_PREFIX = Reco_1:: + +########################## +# Part 37: WEBJOURNAL ## +########################## + +# Specify webjournal categories that have been deleted and we want to redirect +# the articles in the CDS detail view. For example all the articles in deleted +# category `General Information` will redirect to the record detail view. +CFG_WEBJOURNAL_REDIRECT_ARTICLES_OF_DELETED_CATEGORIES = [] + ########################## # THAT's ALL, FOLKS! ## ########################## diff --git a/configure.ac b/configure.ac index 9cce278beb..5a17e890bf 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 \ @@ -874,6 +875,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 \ @@ -923,6 +928,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/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/bibcheck/lib/bibcheck_plugins_unit_tests.py b/modules/bibcheck/lib/bibcheck_plugins_unit_tests.py index 7f0813c557..5b1575f2b8 100644 --- a/modules/bibcheck/lib/bibcheck_plugins_unit_tests.py +++ b/modules/bibcheck/lib/bibcheck_plugins_unit_tests.py @@ -79,7 +79,7 @@ def assertAmends(self, test, changes, **kwargs): record.set_rule(RULE_MOCK) test.check_record(record, **kwargs) self.assertTrue(record.amended) - self.assertEqual(len(record.amendments), len(changes)) + self.assertEqual(len(record._amendments), len(changes)) for field, val in changes.iteritems(): if val is not None: self.assertEqual( @@ -98,7 +98,7 @@ def assertFails(self, test, **kwargs): record.set_rule(RULE_MOCK) test.check_record(record, **kwargs) self.assertFalse(record.valid) - self.assertTrue(len(record.errors) > 0) + self.assertTrue(len(record._errors) > 0) def assertOk(self, test, **kwargs): """ @@ -110,8 +110,8 @@ def assertOk(self, test, **kwargs): test.check_record(record, **kwargs) self.assertTrue(record.valid) self.assertFalse(record.amended) - self.assertEqual(len(record.amendments), 0) - self.assertEqual(len(record.errors), 0) + self.assertEqual(len(record._amendments), 0) + self.assertEqual(len(record._errors), 0) def test_mandatory(self): """ Mandatory fields plugin test """ diff --git a/modules/bibcheck/lib/bibcheck_task.py b/modules/bibcheck/lib/bibcheck_task.py index ad638cb10c..d0f0b337b2 100644 --- a/modules/bibcheck/lib/bibcheck_task.py +++ b/modules/bibcheck/lib/bibcheck_task.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2013, 2014 CERN. +# Copyright (C) 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,8 @@ import time import inspect import itertools +import collections +import functools from collections import namedtuple from ConfigParser import RawConfigParser @@ -50,7 +52,8 @@ CFG_PYLIBDIR, \ CFG_SITE_URL, \ CFG_TMPSHAREDDIR, \ - CFG_CERN_SITE + CFG_CERN_SITE, \ + CFG_SITE_RECORD from invenio.search_engine import \ perform_request_search, \ search_unit_in_bibxxx, \ @@ -63,6 +66,7 @@ from invenio.bibcatalog import BIBCATALOG_SYSTEM from invenio.shellutils import split_cli_ids_arg from invenio.jsonutils import json +from invenio.websearch_webcoll import get_cache_last_updated_timestamp CFG_BATCH_SIZE = 1000 @@ -73,19 +77,169 @@ def __init__(self, rule_name, error): error)) +class Tickets(object): + + """Handle ticket accumulation and dispatching.""" + + def __init__(self, records): + self.records = records + self.policy_method = None + self.ticket_creation_policy = \ + task_get_option('ticket_creation_policy', 'per-record') + + def resolve_ticket_creation_policy(self): + """Resolve the policy for creating tickets.""" + + + known_policies = ('per-rule', + 'per-record', + 'per-rule-per-record', + 'no-tickets') + if self.ticket_creation_policy not in known_policies: + raise Exception("Invalid ticket_creation_policy in config '{0}'". + format(self.ticket_creation_policy)) + + if task_get_option('no_tickets', False): + self.ticket_creation_policy = 'no-tickets' + + policy_translator = { + 'per-rule': self.tickets_per_rule, + 'per-record': self.tickets_per_record, + 'per-rule-per-record': self.tickets_per_rule_per_record + } + self.policy_method = policy_translator[self.ticket_creation_policy] + + @staticmethod + def submit_ticket(msg_subject, msg, record_id): + """Submit a single ticket.""" + if isinstance(msg, unicode): + msg = msg.encode("utf-8") + + submit = functools.partial(BIBCATALOG_SYSTEM.ticket_submit, + subject=msg_subject, text=msg, + queue=task_get_option("queue", "Bibcheck")) + if record_id is not None: + submit = functools.partial(submit, recordid=record_id) + res = submit() + write_message("Bibcatalog returned %s" % res) + if res > 0: + BIBCATALOG_SYSTEM.ticket_comment(None, res, msg) + + def submit(self): + """Generate and submit tickets for the bibcatalog system.""" + self.resolve_ticket_creation_policy() + for ticket_information in self.policy_method(): + self.submit_ticket(*ticket_information) + + def _generate_subject(self, issue_type, record_id, rule_name): + """Generate a fitting subject based on what information is given.""" + assert any((i is not None for i in (issue_type, record_id, rule_name))) + return "[BibCheck{issue_type}]{record_id}{rule_name}".format( + issue_type=":" + issue_type if issue_type else "", + record_id=" [ID:" + record_id + "]" if self.ticket_creation_policy + in ("per-record", "per-rule-per-record") else "", + rule_name=" [Rule:" + rule_name + "]" if self.ticket_creation_policy + in ("per-rule", "per-rule-per-record") else "") + + @staticmethod + def _get_url(record): + """Resolve the URL required to edit a record.""" + return "%s/%s/%s/edit" % (CFG_SITE_URL, CFG_SITE_RECORD, + record.record_id) + + def tickets_per_rule(self): + """Generate with the `per-rule` policy.""" + output = collections.defaultdict(list) + for record in self.records: + for issue in record.issues: + output[issue.rule].append((record, issue.nature, issue.msg)) + for rule_name in output.iterkeys(): + msg = [] + for record, issue_nature, issue_msg in output[rule_name]: + msg.append("{issue_nature}: {issue_msg}".format( + issue_nature=issue_nature, issue_msg=issue_msg)) + msg.append("Edit record ({record_id}) {url}\n".format( + record_id=record.record_id, url=self._get_url(record))) + msg_subject = self._generate_subject(None, None, rule_name) + yield (msg_subject, "\n".join(msg), None) + + def tickets_per_record(self): + """Generate with the `per-record` policy.""" + output = collections.defaultdict(list) + for record in self.records: + for issue in record.issues: + output[record].append((issue.nature, issue.msg)) + for record in output.iterkeys(): + msg = [] + for issue in output[record]: + issue_nature, issue_msg = issue + msg.append("{issue_type}: {rule_messages}". + format(record_id=record.record_id, + issue_type=issue_nature, + rule_messages=issue_msg)) + msg.append("Edit record: {url}".format(url=self._get_url(record))) + msg_subject = self._generate_subject(None, record.record_id, None) + yield (msg_subject, "\n".join(msg), record.record_id) + + def tickets_per_rule_per_record(self): + """Generate with the `per-rule-per-record` policy.""" + output = collections.defaultdict(list) + for record in self.records: + for issue in record.issues: + output[(issue.rule, record)].append((issue.nature, issue.msg)) + for issue_rule, record in output.iterkeys(): + msg = [] + for issue_nature, issue_msg in output[(issue_rule, record)]: + msg.append("{issue_message}".format(issue_message=issue_msg)) + msg.append("Edit record ({record_id}): {url}".format(url=self._get_url(record), + record_id=record.record_id)) + msg_subject = self._generate_subject(issue_nature, record.record_id, + issue_rule) + yield (msg_subject, "\n".join(msg), record.record_id) + + +class Issue(object): + + """Holds information about a single record issue.""" + + def __init__(self, nature, rule, msg): + self._nature = None + self.nature = nature + self.rule = rule + self.msg = msg + + @property + def nature(self): + return self._nature + + @nature.setter + def nature(self, value): + assert value in ('error', 'amendment', 'warning') + self._nature = value + class AmendableRecord(dict): """ Class that wraps a record (recstruct) to pass to a plugin """ def __init__(self, record): dict.__init__(self, record) - self.errors = [] - self.amendments = [] - self.warnings = [] + self.issues = [] self.valid = True self.amended = False self.holdingpen = False self.rule = None self.record_id = self["001"][0][3] + @property + def _errors(self): + return [i for i in self.issues if i.nature == 'error'] + + @property + def _amendments(self): + return [i for i in self.issues if i.nature == 'amendment'] + + @property + def _warnings(self): + return [i for i in self.issues if i.nature == 'warning'] + def iterfields(self, fields, subfield_filter=(None, None)): """ Iterates over marc tags that match a marc expression. @@ -190,17 +344,20 @@ def amend_field(self, position, new_value, message=""): tag, localpos, subfieldpos = position tag = tag.replace("_", " ") - old_value = self._queryval(position) - if new_value != old_value: - if position[2] is None: - fields = self[tag[0:3]] - fields[localpos] = fields[localpos][0:3] + (new_value,) - else: - self._query(position[:2] + (None,))[0][subfieldpos] = (tag[5], new_value) - if message == '': - message = u"Changed field %s from '%s' to '%s'" % (position[0], - old_value.decode('utf-8'), new_value.decode('utf-8')) - self.set_amended(message) + try: + old_value = self._queryval(position) + if new_value != old_value: + if position[2] is None: + fields = self[tag[0:3]] + fields[localpos] = fields[localpos][0:3] + (new_value,) + else: + self._query(position[:2] + (None,))[0][subfieldpos] = (tag[5], new_value) + if message == '': + message = u"Changed field %s from '%s' to '%s'" % (position[0], + old_value.decode('utf-8'), new_value.decode('utf-8')) + self.set_amended(message) + except Exception as err: + self.set_invalid("Error when trying to amend the record at position %s: %s. Maybe there is an empty subfield code?" % (position, err)) def delete_field(self, position, message=""): """ @@ -230,21 +387,24 @@ def set_amended(self, message): """ Mark the record as amended """ write_message("Amended record %s by rule %s: %s" % (self.record_id, self.rule["name"], message)) - self.amendments.append("Rule %s: %s" % (self.rule["name"], message)) + self.issues.append(Issue('amendment', self.rule['name'], message)) self.amended = True if self.rule["holdingpen"]: self.holdingpen = True def set_invalid(self, reason): """ Mark the record as invalid """ - write_message("Record %s marked as invalid by rule %s: %s" % - (CFG_SITE_URL + "/record/%s" % self.record_id, self.rule["name"], reason)) - self.errors.append("Rule %s: %s" % (self.rule["name"], reason)) + url = "{site}/{record}/{record_id}".format(site=CFG_SITE_URL, + record=CFG_SITE_RECORD, + record_id=self.record_id) + write_message("Record {url} marked as invalid by rule {name}: {reason}". + format(url=url, name=self.rule["name"], reason=reason)) + self.issues.append(Issue('error', self.rule['name'], reason)) self.valid = False def warn(self, msg): """ Add a warning to the record """ - self.warnings.append("Rule %s: %s" % (self.rule["name"], msg)) + self.issues.append(Issue('warning', self.rule['name'], msg)) write_message("[WARN] record %s by rule %s: %s" % (self.record_id, self.rule["name"], msg)) @@ -278,8 +438,7 @@ def task_parse_options(key, val, *_): """ Must be defined for bibtask to create a task """ if key in ("--all", "-a"): - for rule_name in val.split(","): - reset_rule_last_run(rule_name) + task_set_option("reset_rules", set(val.split(","))) elif key in ("--enable-rules", "-e"): task_set_option("enabled_rules", set(val.split(","))) elif key in ("--id", "-i"): @@ -288,6 +447,8 @@ def task_parse_options(key, val, *_): task_set_option("queue", val) elif key in ("--no-tickets", "-t"): task_set_option("no_tickets", True) + elif key in ("--ticket-creation-policy", "-p"): + task_set_option("ticket_creation_policy", val) elif key in ("--no-upload", "-b"): task_set_option("no_upload", True) elif key in ("--dry-run", "-n"): @@ -295,6 +456,8 @@ def task_parse_options(key, val, *_): task_set_option("no_tickets", True) elif key in ("--config", "-c"): task_set_option("config", val) + elif key in ("--notimechange", ): + task_set_option("notimechange", True) else: raise StandardError("Error: Unrecognised argument '%s'." % key) return True @@ -305,10 +468,26 @@ def task_run_core(): Returns True when run successfully. False otherwise. """ + rules_to_reset = task_get_option("reset_rules") + if rules_to_reset: + write_message("Resetting the following rules: %s" % rules_to_reset) + for rule in rules_to_reset: + reset_rule_last_run(rule) plugins = load_plugins() rules = load_rules(plugins) + write_message("Loaded rules: %s" % rules, verbose=9) task_set_option('plugins', plugins) recids_for_rules = get_recids_for_rules(rules) + write_message("recids for rules: %s" % recids_for_rules, verbose=9) + + update_database = not (task_has_option('record_ids') or + task_get_option('no_upload', False) or + task_get_option('no_tickets', False)) + + if update_database: + next_starting_dates = {} + for rule_name, rule in rules.iteritems(): + next_starting_dates[rule_name] = get_next_starting_date(rule) all_recids = intbitset([]) single_rules = set() @@ -322,6 +501,7 @@ def task_run_core(): records_to_upload_holdingpen = [] records_to_upload_replace = [] + records_to_submit_tickets = [] for batch in iter_batches(all_recids, CFG_BATCH_SIZE): for rule_name in batch_rules: @@ -335,7 +515,7 @@ def task_run_core(): if len(records): check_records(rule, records) - # Then run them trught normal rules + # Then run them through normal rules for i, record_id, record in batch: progress_percent = int(float(i) / len(all_recids) * 100) task_update_progress("Processing record %s/%s (%i%%)." % @@ -356,8 +536,11 @@ def task_run_core(): records_to_upload_replace.append(record) if not record.valid: - submit_ticket(record, record_id) + records_to_submit_tickets.append(record) + if len(records_to_submit_tickets) >= CFG_BATCH_SIZE: + Tickets(records_to_submit_tickets).submit() + records_to_submit_tickets = [] if len(records_to_upload_holdingpen) >= CFG_BATCH_SIZE: upload_amendments(records_to_upload_holdingpen, True) records_to_upload_holdingpen = [] @@ -366,58 +549,20 @@ def task_run_core(): records_to_upload_replace = [] ## In case there are still some remaining amended records + if records_to_submit_tickets: + Tickets(records_to_submit_tickets).submit() if records_to_upload_holdingpen: upload_amendments(records_to_upload_holdingpen, True) if records_to_upload_replace: upload_amendments(records_to_upload_replace, False) - # Update the database with the last time the rules was ran - for rule in rules.keys(): - update_rule_last_run(rule) - - return True - -def submit_ticket(record, record_id): - """ Submit the errors to bibcatalog """ - - if task_get_option("no_tickets", False): - return - - msg = """ -Bibcheck found some problems with the record with id %s: - -Errors: -%s -Amendments: -%s + # Update the database with the last time each rule was ran + if update_database: + for rule_name, rule in rules.iteritems(): + update_rule_last_run(rule_name, next_starting_dates[rule_name]) -Warnings: -%s - -Edit this record: %s -""" - msg = msg % ( - record_id, - "\n".join(record.errors), - "\n".join(record.amendments), - "\n".join(record.warnings), - "%s/record/%s/edit" % (CFG_SITE_URL, record_id), - ) - if isinstance(msg, unicode): - msg = msg.encode("utf-8") - - subject = "Bibcheck rule failed in record %s" % record_id - - ticket_id = BIBCATALOG_SYSTEM.ticket_submit( - subject=subject, - recordid=record_id, - text=subject, - queue=task_get_option("queue", "Bibcheck") - ) - write_message("Bibcatalog returned %s" % ticket_id) - if ticket_id: - BIBCATALOG_SYSTEM.ticket_comment(None, ticket_id, msg) + return True def upload_amendments(records, holdingpen): @@ -443,7 +588,12 @@ def upload_amendments(records, holdingpen): flag = "-o" else: flag = "-r" - task = task_low_level_submission('bibupload', 'bibcheck', flag, tmp_file) + if task_get_option("notimechange"): + task = task_low_level_submission('bibupload', 'bibcheck', flag, + tmp_file, "--notimechange") + else: + task = task_low_level_submission('bibupload', 'bibcheck', flag, + tmp_file) write_message("Submitted bibupload task %s" % task) def check_record(rule, record): @@ -479,22 +629,45 @@ def get_rule_lastrun(rule_name): return res[0][0] -def update_rule_last_run(rule_name): - """ - Set the last time a rule was run to now. This function should be called - after a rule has been ran. +def get_next_starting_date(rule): + """Calculate the date the next bibcheck run should consider as initial. + + If no filter has been specified then the time that is set is the time the + task was started. Otherwise, it is set to the earliest date among last time + webcoll was run and the last bibindex last_update as the last_run to prevent + records that have yet to be categorized from being perpetually ignored. """ + def dt(t): + return datetime.strptime(t, "%Y-%m-%d %H:%M:%S") + + # Upper limit + task_starting_time = dt(task_get_task_param('task_starting_time')) + + for key, val in rule.iteritems(): + if key.startswith("filter_") and val: + break + else: + return task_starting_time + + # Lower limit + min_last_updated = run_sql("select min(last_updated) from idxINDEX")[0][0] + cache_last_updated = dt(get_cache_last_updated_timestamp()) - if task_has_option('record_ids') or task_get_option('no_upload', False) \ - or task_get_option('no_tickets', False): - return # We don't want to update the database in this case + return min(min_last_updated, task_starting_time, cache_last_updated) - updated = run_sql("UPDATE bibcheck_rules SET last_run=%s WHERE name=%s;", - (task_get_task_param('task_starting_time'), rule_name,)) - if not updated: # rule not in the database, insert it - run_sql("INSERT INTO bibcheck_rules(name, last_run) VALUES (%s, %s)", - (rule_name, task_get_task_param('task_starting_time'))) +def update_rule_last_run(rule_name, next_starting_date): + """ + Set the last time a rule was run. + + This function should be called after a rule has been ran. + """ + next_starting_date_str = datetime.strftime(next_starting_date, + "%Y-%m-%d %H:%M:%S") + + run_sql("""INSERT INTO bibcheck_rules(name, last_run) VALUES (%s, %s) + ON DUPLICATE KEY UPDATE last_run=%s""", + (rule_name, next_starting_date_str, next_starting_date_str)) def reset_rule_last_run(rule_name): """ @@ -779,6 +952,8 @@ def main(): -n, --dry-run Like --no-tickets and --no-upload -c, --config By default bibcheck reads the file rules.cfg. This allows to specify a different config file + --notimechange schedules bibuploads with the option --notimechange + (useful not to trigger reindexing) If any of the options --id, --no-tickets, --no-upload or --dry-run is enabled, bibcheck won't update the last-run-time of a task in the database. @@ -816,9 +991,10 @@ def main(): description="", help_specific_usage=usage, version="Invenio v%s" % CFG_VERSION, - specific_params=("hvtbnV:e:a:i:q:c:", ["help", "version", + specific_params=("hvtbnV:e:a:i:q:c:p:", ["help", "version", "verbose=", "enable-rules=", "all=", "id=", "queue=", - "no-tickets", "no-upload", "dry-run", "config"]), + "no-tickets", "no-upload", "dry-run", "config", + "notimechange", "ticket-creation-policy="]), task_submit_elaborate_specific_parameter_fnc=task_parse_options, task_run_fnc=task_run_core) diff --git a/modules/bibcheck/lib/bibcheck_unit_tests.py b/modules/bibcheck/lib/bibcheck_unit_tests.py index 6a02504dc5..6ebcd71a39 100644 --- a/modules/bibcheck/lib/bibcheck_unit_tests.py +++ b/modules/bibcheck/lib/bibcheck_unit_tests.py @@ -146,15 +146,23 @@ def test_valid(self): self.assertTrue(self.record.valid) self.record.set_invalid("test message") self.assertFalse(self.record.valid) - self.assertEqual(self.record.errors, ["Rule test_rule: test message"]) + self.assertEqual(len(self.record._errors), 1) + error = self.record._errors[0] + self.assertEqual(error.nature, "error") + self.assertEqual(error.rule, "test_rule") + self.assertEqual(error.msg, "test message") def test_amend(self): """ Test the amend method """ - self.assertFalse(self.record.amendments) + self.assertFalse(self.record._amendments) self.record.amend_field(("100__a", 0, 0), "Pepe", "Changed author") self.assertEqual(self.record["100"][0][0][0][1], "Pepe") self.assertTrue(self.record.amended) - self.assertEqual(self.record.amendments, ["Rule test_rule: Changed author"]) + self.assertEqual(len(self.record._amendments), 1) + amendment = self.record._amendments[0] + self.assertEqual(amendment.nature, "amendment") + self.assertEqual(amendment.rule, "test_rule") + self.assertEqual(amendment.msg, "Changed author") def test_itertags(self): """ Test the itertags method """ 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' CFG_BIBCIRCULATION_LIBRARIAN_EMAIL = 'CERN Library Desk' 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..3141505563 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. @@ -2700,6 +2721,11 @@ def get_purchase_request_borrower_details(ill_request_id): else: return None +def update_ill_request_letter_number(ill_request_id, overdue_letter_number): + query = ('UPDATE crcILLREQUEST set overdue_letter_number=%s ' + 'where id=%s') + run_sql(query, (overdue_letter_number, ill_request_id)) + def update_ill_request(ill_request_id, library_id, request_date, expected_date, arrival_date, due_date, return_date, status, cost, barcode, library_notes): diff --git a/modules/bibcirculation/lib/bibcirculation_templates.py b/modules/bibcirculation/lib/bibcirculation_templates.py index 376538764d..9e5e33b164 100644 --- a/modules/bibcirculation/lib/bibcirculation_templates.py +++ b/modules/bibcirculation/lib/bibcirculation_templates.py @@ -2246,8 +2246,8 @@ def tmpl_loan_on_desk_step1(self, result, key, string, infos, name = user_info[0] user_id = user_info[2] out += """ -