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/bibcatalog/lib/bibcatalog_system_email.py b/modules/bibcatalog/lib/bibcatalog_system_email.py index a80d20eca5..480de52f61 100644 --- a/modules/bibcatalog/lib/bibcatalog_system_email.py +++ b/modules/bibcatalog/lib/bibcatalog_system_email.py @@ -56,7 +56,7 @@ def ticket_search(self, uid, recordid=-1, subject="", text="", creator="", owner raise NotImplementedError - def ticket_submit(self, uid=None, subject="", recordid=-1, text="", queue="", priority="", owner="", requestor=""): + def ticket_submit(self, uid=None, **kwargs): """creates a ticket. Returns ticket_id on success, otherwise None""" if not EMAIL_SUBMIT_CONFIGURED: @@ -65,32 +65,30 @@ def ticket_submit(self, uid=None, subject="", recordid=-1, text="", queue="", pr prefix="please configure bibcatalog email sending in CFG_BIBCATALOG_SYSTEM and CFG_BIBCATALOG_SYSTEM_EMAIL_ADDRESS") ticket_id = self._get_ticket_id() - priorityset = "" - queueset = "" - requestorset = "" - ownerset = "" - recidset = " cf-recordID: %s\n" % recordid - textset = "" - subjectset = "" - if subject: - subjectset = 'ticket #%s - %s' % (ticket_id, subject) - if priority: - priorityset = " priority: %s\n" % priority - if queue: - queueset = " queue: %s\n" % queue - if requestor: - requestorset = " requestor: %s\n" % requestor + textset = '' + ownerset = '' + priorityset = ' priority: {0}\n'.format(kwargs['priority']) if 'priority' in kwargs else "" + queueset = ' queue: {0}\n'.format(kwargs['queue']) if 'queue' in kwargs else "" + requestorset = ' requestor: {0}\n'.format(kwargs['requestor']) if 'requestor' in kwargs else "" + recidset = ' cf-recordID: {0}\n'.format(kwargs.get('recordid', -1)) + subjectset = 'ticket #{0} - {1}'.format(ticket_id, kwargs['subject']) if 'subject' in kwargs else '' + owner = kwargs.get('owner', '') if owner: ownerprefs = invenio.webuser.get_user_preferences(owner) - if ownerprefs.has_key("bibcatalog_username"): - owner = ownerprefs["bibcatalog_username"] - ownerset = " owner: %s\n" % owner + if ownerprefs.has_key('bibcatalog_username'): + owner = ownerprefs['bibcatalog_username'] + ownerset = ' owner: {0}\n'.format(owner) textset += ownerset + requestorset + recidset + queueset + priorityset + '\n' - textset += text + '\n' + textset += kwargs.get('text', '') + '\n' - ok = send_email(fromaddr=FROM_ADDRESS, toaddr=TO_ADDRESS, subject=subjectset, content=textset) + ok = send_email( + fromaddr=kwargs.get('from_address', FROM_ADDRESS), + toaddr=kwargs.get('to_address', TO_ADDRESS), + subject=subjectset, + content=textset + ) if ok: return ticket_id return None 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..4d1be56811 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,170 @@ 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, **kwargs): + """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(**kwargs) + write_message("Bibcatalog returned {0}".format(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, kwargs in self.policy_method(): + self.submit_ticket(*ticket_information, **kwargs) + + 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['name']].append((record, issue.nature, issue.msg, issue.rule)) + for rule_name in output.iterkeys(): + msg = [] + for record, issue_nature, issue_msg, rule 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), rule + + 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, issue.rule)) + for record in output.iterkeys(): + msg = [] + for issue in output[record]: + issue_nature, issue_msg, rule = 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), rule + + 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, issue.rule)) + for issue_rule, record, rule 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), rule + + +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.warn = False 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 +345,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=""): """ @@ -226,27 +384,32 @@ def add_subfield(self, position, code, value): self.set_amended("Added subfield %s='%s' to field %s" % (code, value, position[0][:5])) - def set_amended(self, message): + def set_amended(self, message, warn=False): """ 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.record_id, self.rule['name'], message)) + self.issues.append(Issue('amendment', self.rule, message)) self.amended = True if self.rule["holdingpen"]: self.holdingpen = True + self.warn = warn if warn else self.warn 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, 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, msg)) write_message("[WARN] record %s by rule %s: %s" % - (self.record_id, self.rule["name"], msg)) + (self.record_id, self.rule, msg)) + self.warn = True def set_rule(self, rule): """ Set the current rule the record is been checked against """ @@ -278,8 +441,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 +450,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 +459,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 +471,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 +504,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 +518,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%%)." % @@ -355,9 +538,12 @@ def task_run_core(): else: records_to_upload_replace.append(record) - if not record.valid: - submit_ticket(record, record_id) + if not record.valid or record.warn: + 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 +552,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 +591,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,23 +632,46 @@ 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") - 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 + # Upper limit + task_starting_time = dt(task_get_task_param('task_starting_time')) - 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'))) + 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()) + + return min(min_last_updated, task_starting_time, cache_last_updated) +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): """ Reset the last time a rule was run. This will cause the rule to be @@ -557,7 +733,8 @@ def parse_arg(argument_str, arg_name): if key in ("filter_pattern", "filter_field", "filter_collection", - "filter_limit"): + "filter_limit", + "to_address"): rule[key] = val elif key in ("holdingpen", "consider_deleted_records"): @@ -622,11 +799,9 @@ def get_recids_for_rules(rules): for rule_name, rule in rules.iteritems(): if "filter_pattern" in rule: query = rule["filter_pattern"] - if "filter_collection" in rule: - collections = rule["filter_collection"].split() - else: - collections = None - write_message("Performing given search query: '%s'" % query) + collections = rule["filter_collection"].split(',') \ + if 'filter_collection' in rule else [] + write_message("Performing given search query: '{0}', '{1}'".format(query, collections)) if collections: result = perform_request_search( p=query, @@ -736,6 +911,8 @@ def print_rules(): print " - Filter: %s" % rule["filter_pattern"] if "filter_collection" in rule: print " - Filter collection: %s" % rule["filter_collection"] + if "to_address" in rule: + print " - To Address: %s" % rule["to_address"] print " - Checker: %s" % rule["check"] if len(rule["checker_params"]) > 0: print " Parameters:" @@ -779,6 +956,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 +995,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_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..98ca9d6b2f 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'] @@ -3041,36 +3044,35 @@ def update_item_info_step6(req, tup_infos, ln=CFG_SITE_LANG): else: infos.append(_("Item [%s] updated, but the barcode was not modified because it was not found (!?).") % (old_barcode)) - copies = db.get_item_copies_details(recid) - requests = db.get_item_requests(recid) - loans = db.get_item_loans(recid) - purchases = db.get_item_purchases(CFG_BIBCIRCULATION_ACQ_STATUS_NEW, recid) + copies = db.get_item_copies_details(recid) + requests = db.get_item_requests(recid) + loans = db.get_item_loans(recid) + purchases = db.get_item_purchases(CFG_BIBCIRCULATION_ACQ_STATUS_NEW, recid) - req_hist_overview = db.get_item_requests_historical_overview(recid) - loans_hist_overview = db.get_item_loans_historical_overview(recid) - purchases_hist_overview = db.get_item_purchases(CFG_BIBCIRCULATION_ACQ_STATUS_RECEIVED, recid) + req_hist_overview = db.get_item_requests_historical_overview(recid) + loans_hist_overview = db.get_item_loans_historical_overview(recid) + purchases_hist_overview = db.get_item_purchases(CFG_BIBCIRCULATION_ACQ_STATUS_RECEIVED, recid) - body = bc_templates.tmpl_get_item_details(recid=recid, - copies=copies, - requests=requests, - loans=loans, - purchases=purchases, - req_hist_overview=req_hist_overview, - loans_hist_overview=loans_hist_overview, - purchases_hist_overview=purchases_hist_overview, - infos=infos, - ln=ln) + infos.append(_('If you wish, you can now open the Record Editor to modify the bibliographic information of this item.') % (CFG_SITE_SECURE_URL + "/record/edit/#state=edit&recid=" + str(recid))) - return page(title=_("Update item information"), - uid=id_user, - req=req, - body=body, language=ln, - navtrail=navtrail_previous_links, - lastupdated=__lastupdated__) + body = bc_templates.tmpl_get_item_details(recid=recid, + copies=copies, + requests=requests, + loans=loans, + purchases=purchases, + req_hist_overview=req_hist_overview, + loans_hist_overview=loans_hist_overview, + purchases_hist_overview=purchases_hist_overview, + infos=infos, + ln=ln) + + return page(title=_("Update item information"), + uid=id_user, + req=req, + body=body, language=ln, + navtrail=navtrail_previous_links, + lastupdated=__lastupdated__) - else: - return redirect_to_url(req, CFG_SITE_SECURE_URL + - "/record/edit/#state=edit&recid=" + str(recid)) def item_search(req, infos=[], ln=CFG_SITE_LANG): """ @@ -4306,6 +4308,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 +4563,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 +4607,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) @@ -4936,6 +4968,25 @@ def ill_request_details_step2(req, delete_key, ill_request_id, new_status, barcode = db.get_ill_barcode(ill_request_id) db.update_ill_loan_status(borrower_id, barcode, return_date, 'ill') + # ill recall letter issue + try: + from invenio.dbquery import run_sql + _query = ('SELECT due_date from crcILLREQUEST where id = %s') + _due = run_sql(_query, (ill_request_id))[0][0] + + # Since we don't know if the due_date is a string or datetime + try: + _due_date = datetime.datetime.strptime(due_date, '%Y-%m-%d') + except TypeError: + _due_date = due_date + + # This means that the ILL got extended, we therefore reset the + # overdue_letter_numer + if _due < _due_date: + db.update_ill_request_letter_number(ill_request_id, 0) + except Exception: + pass + db.update_ill_request(ill_request_id, library_id, request_date, expected_date, arrival_date, due_date, return_date, new_status, cost, barcode, diff --git a/modules/bibconvert/etc/oaiarxiv2marcxml.xsl b/modules/bibconvert/etc/oaiarxiv2marcxml.xsl index c627ceb381..95122a74c8 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 + + + + + + diff --git a/modules/bibdocfile/lib/bibdocfile.py b/modules/bibdocfile/lib/bibdocfile.py index c45f824163..61e1b17949 100644 --- a/modules/bibdocfile/lib/bibdocfile.py +++ b/modules/bibdocfile/lib/bibdocfile.py @@ -1,5 +1,5 @@ # This file is part of Invenio. -# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 CERN. +# Copyright (C) 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 @@ -59,6 +59,7 @@ import cgi import sys import copy +import tarfile if sys.hexversion < 0x2060000: from md5 import md5 @@ -94,6 +95,7 @@ from sets import Set as set # pylint: enable=W0622 +#from invenio.webstat import register_customevent from invenio.shellutils import escape_shell_arg, run_shell_command from invenio.dbquery import run_sql, DatabaseError from invenio.errorlib import register_exception @@ -102,6 +104,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 @@ -120,10 +123,11 @@ CFG_BIBDOCFILE_ENABLE_BIBDOCFSINFO_CACHE, \ CFG_BIBDOCFILE_ADDITIONAL_KNOWN_MIMETYPES, \ CFG_BIBDOCFILE_PREFERRED_MIMETYPES_MAPPING, \ - CFG_BIBCATALOG_SYSTEM + CFG_BIBCATALOG_SYSTEM, \ + 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 + CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT, CFG_BIBDOCFILE_STREAM_ARCHIVE_FORMATS from invenio.pluginutils import PluginContainer import invenio.template @@ -787,6 +791,7 @@ def get_xml_8564(self): url = afile.get_url() description = afile.get_description() comment = afile.get_comment() + out += '\t\t%s\n' % afile.get_bibdocid() if url: out += '\t\t%s\n' % encode_for_xml(url) if description: @@ -797,17 +802,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): @@ -1121,7 +1131,8 @@ def add_bibdoc(self, doctype="Main", docname='file', never_fail=False): def add_new_file(self, fullpath, doctype="Main", docname=None, never_fail=False, description=None, comment=None, - docformat=None, flags=None, modification_date=None): + copyright=None, license=None, docformat=None, + flags=None, modification_date=None): """ Directly add a new file to this record. @@ -1152,6 +1163,10 @@ def add_new_file(self, fullpath, doctype="Main", docname=None, @type description: string @param comment: an optional comment to the file. @type comment: string + @param copyright: an optional copyright of the file. + @type copyright: string + @param license: an optional license of the file. + @type license: string @param format: the extension of the file. If not specified it will be guessed (see L{guess_format_from_url}). @type format: string @@ -1173,7 +1188,7 @@ def add_new_file(self, fullpath, doctype="Main", docname=None, except InvenioBibDocFileError: # bibdoc doesn't already exists! bibdoc = self.add_bibdoc(doctype, docname, False) - bibdoc.add_file_new_version(fullpath, description=description, comment=comment, docformat=docformat, flags=flags, modification_date=modification_date) + bibdoc.add_file_new_version(fullpath, description=description, comment=comment, copyright=copyright, license=license, docformat=docformat, flags=flags, modification_date=modification_date) else: try: bibdoc.add_file_new_format(fullpath, description=description, comment=comment, docformat=docformat, flags=flags, modification_date=modification_date) @@ -1181,12 +1196,12 @@ def add_new_file(self, fullpath, doctype="Main", docname=None, # Format already exist! if never_fail: bibdoc = self.add_bibdoc(doctype, docname, True) - bibdoc.add_file_new_version(fullpath, description=description, comment=comment, docformat=docformat, flags=flags, modification_date=modification_date) + bibdoc.add_file_new_version(fullpath, description=description, comment=comment, copyright=copyright, license=license, docformat=docformat, flags=flags, modification_date=modification_date) else: raise return bibdoc - def add_new_version(self, fullpath, docname=None, description=None, comment=None, docformat=None, flags=None): + def add_new_version(self, fullpath, docname=None, description=None, comment=None, copyright=None, license=None, docformat=None, flags=None): """ Adds a new file to an already existent document object as a new version. @@ -1200,6 +1215,10 @@ def add_new_version(self, fullpath, docname=None, description=None, comment=None @type description: string @param comment: an optional comment to the file. @type comment: string + @param copyright: an optional copyright of the file. + @type copyright: string + @param license: an optional license of the file. + @type license: string @param format: the extension of the file. If not specified it will be guessed (see L{guess_format_from_url}). @type format: string @@ -1221,7 +1240,7 @@ def add_new_version(self, fullpath, docname=None, description=None, comment=None if 'pdfa' in get_subformat_from_format(docformat).split(';') and not 'PDF/A' in flags: flags.append('PDF/A') bibdoc = self.get_bibdoc(docname=docname) - bibdoc.add_file_new_version(fullpath, description=description, comment=comment, docformat=docformat, flags=flags) + bibdoc.add_file_new_version(fullpath, description=description, comment=comment, copyright=copyright, license=license, docformat=docformat, flags=flags) return bibdoc def add_new_format(self, fullpath, docname=None, description=None, comment=None, docformat=None, flags=None, modification_date=None): @@ -1564,6 +1583,36 @@ def get_text(self, extract_text_if_necessary=True): return " ".join(texts) + def stream_archive_of_latest_files(self, req, files_size=''): + """ + Streams the tar archive with all files of a certain file size (that + are not restricted or hidden) to the user. + File size should be a string that can be compared with the output of + BibDocFile.get_subformat() function. + + @param req: Apache Request Object + @type req: Apache Request Object + @param files_size: size of the files (they can be defined in + bibdocfile_config). Empty string means the original size. + @type files_size: string + """ + # Get the internal size from the user-friendly file size name + internal_format = [f[1] for f in CFG_BIBDOCFILE_STREAM_ARCHIVE_FORMATS if f[0] == files_size] + if len(internal_format) < 1: + # Incorrect file size + return + internal_format = internal_format[0] + tarname = str(self.id) + "_" + files_size + '.tar' + + # Select files that user can download (not hidden nor restricted) + 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 and not f.hidden: + tar.add(f.get_path(), arcname=f.get_full_name(), recursive=False) + tar.close() class BibDoc(object): """ @@ -1962,7 +2011,7 @@ def set_status(self, new_status): self.status = new_status self.touch('status') - def add_file_new_version(self, filename, description=None, comment=None, docformat=None, flags=None, modification_date=None): + def add_file_new_version(self, filename, description=None, comment=None, copyright=None, license=None, docformat=None, flags=None, modification_date=None): """ Add a new version of a file. If no physical file is already attached to the document a the given file will have version 1. Otherwise the @@ -1974,6 +2023,10 @@ def add_file_new_version(self, filename, description=None, comment=None, docform @type description: string @param comment: an optional comment to the file. @type comment: string + @param copyright: an optional copyright of the file. + @type copyright: string + @param license: an optional license of the file. + @type license: string @param format: the extension of the file. If not specified it will be retrieved from the filename (see L{decompose_file}). @type format: string @@ -2008,6 +2061,8 @@ def add_file_new_version(self, filename, description=None, comment=None, docform raise InvenioBibDocFileError("Encountered an exception while copying '%s' to '%s': '%s'" % (filename, destination, e)) self.more_info.set_description(description, docformat, myversion) self.more_info.set_comment(comment, docformat, myversion) + self.more_info.set_copyright(copyright) + self.more_info.set_license(license) if flags is None: flags = [] if 'pdfa' in get_subformat_from_format(docformat).split(';') and not 'PDF/A' in flags: @@ -2029,7 +2084,7 @@ def add_file_new_version(self, filename, description=None, comment=None, docform run_sql("INSERT INTO bibdocfsinfo(id_bibdoc, version, format, last_version, cd, md, checksum, filesize, mime) VALUES(%s, %s, %s, true, %s, %s, %s, %s, %s)", (self.id, myversion, docformat, just_added_file.cd, just_added_file.md, just_added_file.get_checksum(), just_added_file.get_size(), just_added_file.mime)) run_sql("UPDATE bibdocfsinfo SET last_version=false WHERE id_bibdoc=%s AND version<%s", (self.id, myversion)) - def add_file_new_format(self, filename, version=None, description=None, comment=None, docformat=None, flags=None, modification_date=None): + def add_file_new_format(self, filename, version=None, description=None, comment=None, copyright=None, license=None, docformat=None, flags=None, modification_date=None): """ Add a file as a new format. @@ -2042,6 +2097,10 @@ def add_file_new_format(self, filename, version=None, description=None, comment= @type description: string @param comment: an optional comment to the file. @type comment: string + @param copyright: an optional copyright of the file. + @type copyright: string + @param license: an optional license of the file. + @type license: string @param format: the extension of the file. If not specified it will be retrieved from the filename (see L{decompose_file}). @type format: string @@ -2077,6 +2136,10 @@ def add_file_new_format(self, filename, version=None, description=None, comment= raise InvenioBibDocFileError, "Encountered an exception while copying '%s' to '%s': '%s'" % (filename, destination, e) self.more_info.set_comment(comment, docformat, version) self.more_info.set_description(description, docformat, version) + if version == 1: + # only when the new BibDoc is created, we modify copyright, license while adding new format + self.more_info.set_copyright(copyright) + self.more_info.set_license(license) if flags is None: flags = [] if 'pdfa' in get_subformat_from_format(docformat).split(';') and not 'PDF/A' in flags: @@ -2127,6 +2190,8 @@ def purge(self): if afile.get_version() < version: self.more_info.unset_comment(afile.get_format(), afile.get_version()) self.more_info.unset_description(afile.get_format(), afile.get_version()) + # since the last format is not erased, we don't unset + # the copyright and license here for flag in CFG_BIBDOCFILE_AVAILABLE_FLAGS: self.more_info.unset_flag(flag, afile.get_format(), afile.get_version()) try: @@ -2173,7 +2238,13 @@ def revert(self, version): version = int(version) docfiles = self.list_version_files(version) if docfiles: - self.add_file_new_version(docfiles[0].get_full_path(), description=docfiles[0].get_description(), comment=docfiles[0].get_comment(), docformat=docfiles[0].get_format(), flags=docfiles[0].flags) + self.add_file_new_version(docfiles[0].get_full_path(), + description=docfiles[0].get_description(), + comment=docfiles[0].get_comment(), + copyright=docfiles[0].get_copyright(), + license=docfiles[0].get_license(), + docformat=docfiles[0].get_format(), + flags=docfiles[0].flags) for docfile in docfiles[1:]: self.add_file_new_format(docfile.filename, description=docfile.get_description(), comment=docfile.get_comment(), docformat=docfile.get_format(), flags=docfile.flags) @@ -2373,6 +2444,28 @@ def set_description(self, description, docformat, version=None): self.more_info.set_description(description, docformat, version) self.dirty = True + def set_copyright(self, copyright): + """ + Updates the copyright of the document. + + @param copyright: the new copyright. + @type copyright: dict + """ + self.more_info.set_copyright(copyright) + self.touch() + self._build_file_list('init') + + def set_license(self, license): + """ + Updates the license of the document. + + @param license: the new license. + @type license: dict + """ + self.more_info.set_license(license) + self.touch() + self._build_file_list('init') + def set_flag(self, flagname, docformat, version=None): """ Sets a flag for a specific format/version of the document. @@ -2467,6 +2560,18 @@ def get_description(self, docformat, version=None): docformat = normalize_format(docformat) return self.more_info.get_description(docformat, version) + def get_copyright(self): + """ + Retrieve the copyright of a document. + """ + return self.more_info.get_copyright() + + def get_license(self): + """ + Retrieve the license of a document. + """ + return self.more_info.get_license() + def hidden_p(self, docformat, version=None): """ Returns True if the file specified by the given format/version is @@ -2801,12 +2906,28 @@ 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 + and hidden files are not counted, unless there is no user_info. + @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() + # First check for restrictions + if all_files or (bibdocfile.is_restricted(user_info)[0] == 0 and not bibdocfile.hidden): + # Then check if the format is correct + if subformat is None or bibdocfile.get_subformat() == subformat: + ret += bibdocfile.get_size() return ret def get_total_size(self): @@ -2847,7 +2968,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) @@ -2856,12 +2978,27 @@ 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,)) + + try: + from invenio.webstat import register_customevent + ## register event in webstat + download_register_event = [ + recid, self.id, version, docformat, userid, ip_address, + user_agent + ] + is_bot = False + if user_agent: + for bot in CFG_ELASTICSEARCH_BOT_AGENT_STRINGS: + if bot in user_agent: + is_bot = True + break + download_register_event.append(is_bot) + register_customevent("downloads", download_register_event) + except: + register_exception( + ("Do the webstat tables exists? Try with 'webstatadmin" + " --load-config'") + ) def get_incoming_relations(self, rel_type=None): """Return all relations in which this BibDoc appears on target position @@ -2951,10 +3088,14 @@ def __init__(self, fullpath, recid_doctypes, version, docformat, docid, self.description = more_info.get_description(docformat, version) self.comment = more_info.get_comment(docformat, version) self.flags = more_info.get_flags(docformat, version) + self.copyright = more_info.get_copyright() + self.license = more_info.get_license() else: self.description = None self.comment = None self.flags = [] + self.copyright = {} + self.license = {} self.format = normalize_format(docformat) self.superformat = get_superformat_from_format(self.format) self.subformat = get_subformat_from_format(self.format) @@ -2974,12 +3115,14 @@ def __init__(self, fullpath, recid_doctypes, version, docformat, docid, self.cd = self.md self.dir = os.path.dirname(fullpath) + # make filename url safe + url_safe_filename = urllib.quote(self.name) if self.subformat: - self.url = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], self.name, self.superformat), {'subformat' : self.subformat}) - self.fullurl = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], self.name, self.superformat), {'subformat' : self.subformat, 'version' : self.version}) + self.url = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], url_safe_filename, self.superformat), {'subformat' : self.subformat}) + self.fullurl = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], url_safe_filename, self.superformat), {'subformat' : self.subformat, 'version' : self.version}) else: - self.url = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], self.name, self.superformat), {}) - self.fullurl = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], self.name, self.superformat), {'version' : self.version}) + self.url = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], url_safe_filename, self.superformat), {}) + self.fullurl = create_url('%s/%s/%s/files/%s%s' % (CFG_SITE_URL, CFG_SITE_RECORD, self.recids_doctypes[0][0], url_safe_filename, self.superformat), {'version' : self.version}) self.etag = '"%i%s%i"' % (self.docid, self.format, self.version) self.magic = None @@ -3117,6 +3260,12 @@ def get_description(self): def get_comment(self): return self.comment + def get_copyright(self): + return self.copyright + + def get_license(self): + return self.license + def get_content(self): """Returns the binary content of the file.""" content_fd = open(self.fullpath, 'rb') @@ -4316,10 +4465,13 @@ class BibDocMoreInfo(MoreInfo): This class wraps contextual information of the documents, such as the - comments - descriptions - - flags. + - flags + - copyright + - license. Such information is kept separately per every format/version instance of - the corresponding document and is searialized in the database, ready - to be retrieved (but not searched). + the corresponding document (except for the copyright and license which are + the same for each version and format of a bibdoc) and is searialized in + the database, ready to be retrieved (but not searched). @param docid: the document identifier. @type docid: integer @@ -4348,6 +4500,10 @@ def __init__(self, docid, cache_only = False, initial_data = None): self['comments'] = {} if 'flags' not in self: self['flags'] = {} + if 'copyright' not in self: + self['copyright'] = {} + if 'license' not in self: + self['license'] = {} if DBG_LOG_QUERIES: from invenio.bibtask import write_message write_message("Creating BibDocMoreInfo :" + repr(self["comments"])) @@ -4389,6 +4545,26 @@ def set_flag(self, flagname, docformat, version): raise ValueError, "%s is not in %s" % \ (flagname, CFG_BIBDOCFILE_AVAILABLE_FLAGS) + def get_copyright(self): + """ + Returns the copyright. + """ + try: + return self['copyright'] + except: + register_exception() + raise + + def get_license(self): + """ + Returns the license. + """ + try: + return self['license'] + except: + register_exception() + raise + def get_comment(self, docformat, version): """ Returns the specified comment. @@ -4526,6 +4702,49 @@ def set_description(self, description, docformat, version): register_exception() raise + def set_copyright(self, copyright): + """ + Set the copyright + + @param copyright: dictionary with copyright information + @type copyright: dictionary + """ + try: + if copyright == KEEP_OLD_VALUE: + copyright = self.get_copyright() + if not copyright: + self.unset_copyright() + return + + # Just to make sure that dictionary has proper keys + if set(('copyright_holder', 'copyright_date', 'copyright_message', 'copyright_holder_contact')) <= set(copyright): + self['copyright'] = copyright + self.set_data("", 'copyright', copyright) + except: + register_exception() + raise + + def set_license(self, license): + """ + Set the license + + @param license: dictionary with license information + @type license: dictionary + """ + try: + if license == KEEP_OLD_VALUE: + license = self.get_license() + if not license: + self.unset_license() + return + # Just to make sure that dictionary has proper keys + if set(('license', 'license_url', 'license_body')) <= set(license): + self['license'] = license + self.set_data("", 'license', license) + except: + register_exception() + raise + def unset_comment(self, docformat, version): """ Unset a comment. @@ -4566,6 +4785,26 @@ def unset_description(self, docformat, version): register_exception() raise + def unset_copyright(self): + """ + Unset a copyright. + """ + try: + copyright = {} + except: + register_exception() + raise + + def unset_license(self): + """ + Unset a license. + """ + try: + license = {} + except: + register_exception() + raise + def unset_flag(self, flagname, docformat, version): """ Unset a flag. diff --git a/modules/bibdocfile/lib/bibdocfile_config.py b/modules/bibdocfile/lib/bibdocfile_config.py index 0caa8ddbd2..69f3f1ca17 100644 --- a/modules/bibdocfile/lib/bibdocfile_config.py +++ b/modules/bibdocfile/lib/bibdocfile_config.py @@ -22,6 +22,8 @@ except ImportError: CFG_BIBDOCFILE_DOCUMENT_FILE_MANAGER_MISC = { 'can_revise_doctypes': ['*'], + 'can_change_copyright_doctypes': ['*'], + 'can_change_advanced_copyright_doctypes': ['*'], 'can_comment_doctypes': ['*'], 'can_describe_doctypes': ['*'], 'can_delete_doctypes': ['*'], @@ -69,3 +71,12 @@ # CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT -- this is the default subformat used # when creating new icons. CFG_BIBDOCFILE_DEFAULT_ICON_SUBFORMAT = "icon" + +# CFG_BIBDOCFILE_STREAM_ARCHIVE_FORMATS -- 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_STREAM_ARCHIVE_FORMATS = [ + ('small', 'icon-180'), + ('medium', 'icon-640'), + ('large', 'icon-1440'), + ('original', '')] diff --git a/modules/bibdocfile/lib/bibdocfile_managedocfiles.py b/modules/bibdocfile/lib/bibdocfile_managedocfiles.py index 061baa5b41..a1695be11b 100644 --- a/modules/bibdocfile/lib/bibdocfile_managedocfiles.py +++ b/modules/bibdocfile/lib/bibdocfile_managedocfiles.py @@ -73,6 +73,11 @@ import os import time import cgi +import sys +if sys.hexversion < 0x2060000: + import simplejson as json +else: + import json from urllib import urlencode @@ -97,7 +102,9 @@ from invenio.urlutils import create_html_mailto from invenio.htmlutils import escape_javascript_string from invenio.bibtask import task_low_level_submission, bibtask_allocate_sequenceid -CFG_ALLOWED_ACTIONS = ['revise', 'delete', 'add', 'addFormat'] +from invenio.bibfield import get_record +from invenio.bibknowledge import get_kbr_items +CFG_ALLOWED_ACTIONS = ['revise', 'delete', 'add', 'addFormat', 'copyrightChange'] params_id = 0 @@ -111,6 +118,8 @@ def create_file_upload_interface(recid, doctypes_and_desc=None, can_delete_doctypes=None, can_revise_doctypes=None, + can_change_copyright_doctypes=None, + can_change_advanced_copyright_doctypes=None, can_describe_doctypes=None, can_comment_doctypes=None, can_keep_doctypes=None, @@ -121,6 +130,8 @@ def create_file_upload_interface(recid, keep_default=True, show_links=True, file_label=None, filename_label=None, description_label=None, comment_label=None, + copyright_label=None, + advanced_copyright_label=None, restrictions_and_desc=None, can_restrict_doctypes=None, restriction_label=None, @@ -212,6 +223,20 @@ def create_file_upload_interface(recid, Use ['*'] for "all doctypes" @type can_revise_doctypes: list(string) + @param can_change_copyright_doctypes: the list of doctypes for which users + are allowed to select predefined + copyright and license + Eg: ['main', 'additional'] + Use ['*'] for "all doctypes" + @type can_change_copyright_doctypes: list(string) + + @param can_change_advanced_copyright_doctypes: the list of doctypes for + which users are allowed to manually + edit copyright and license + Eg: ['main', 'additional'] + Use ['*'] for "all doctypes" + @type can_change_advanced_copyright_doctypes: list(string) + @param can_describe_doctypes: the list of doctypes that users are allowed to describe Eg: ['main', 'additional'] @@ -327,6 +352,12 @@ def create_file_upload_interface(recid, @param comment_label: the label for the comments field @type comment_label: string + @param copyright_label: the label for the copyright field + @type copyright_label: string + + @param advanced_copyright_label: the label for the advanced copyright link + @type advanced_copyright_label: string + @param restriction_label: the label in front of the restrictions list @type restriction_label: string @@ -400,6 +431,10 @@ def create_file_upload_interface(recid, description_label = _('Description') if not comment_label: comment_label = _('Comment') + if not copyright_label: + copyright_label = _('Copyright and license') + if not advanced_copyright_label: + advanced_copyright_label = _('Advanced') if not restriction_label: restriction_label = _('Access') if not doctypes_and_desc: @@ -408,6 +443,10 @@ def create_file_upload_interface(recid, can_delete_doctypes = [] if not can_revise_doctypes: can_revise_doctypes = [] + if not can_change_copyright_doctypes: + can_change_copyright_doctypes = [] + if not can_change_advanced_copyright_doctypes: + can_change_advanced_copyright_doctypes = [] if not can_describe_doctypes: can_describe_doctypes = [] if not can_comment_doctypes: @@ -488,14 +527,16 @@ def create_file_upload_interface(recid, parameters = _read_file_revision_interface_configuration_from_disk(working_dir) (minsize, maxsize, doctypes_and_desc, doctypes, can_delete_doctypes, can_revise_doctypes, + can_change_copyright_doctypes, + can_change_advanced_copyright_doctypes, can_describe_doctypes, can_comment_doctypes, can_keep_doctypes, can_rename_doctypes, can_add_format_to_doctypes, create_related_formats, can_name_new_files, keep_default, show_links, file_label, filename_label, description_label, - comment_label, restrictions_and_desc, - can_restrict_doctypes, + comment_label, copyright_label, advanced_copyright_label, + restrictions_and_desc, can_restrict_doctypes, restriction_label, doctypes_to_default_filename, max_files_for_doctype, print_outside_form_tag, display_hidden_files, protect_hidden_files, @@ -505,14 +546,16 @@ def create_file_upload_interface(recid, # disk for later reuse parameters = (minsize, maxsize, doctypes_and_desc, doctypes, can_delete_doctypes, can_revise_doctypes, + can_change_copyright_doctypes, + can_change_advanced_copyright_doctypes, can_describe_doctypes, can_comment_doctypes, can_keep_doctypes, can_rename_doctypes, can_add_format_to_doctypes, create_related_formats, can_name_new_files, keep_default, show_links, file_label, filename_label, description_label, - comment_label, restrictions_and_desc, - can_restrict_doctypes, + comment_label, copyright_label, advanced_copyright_label, + restrictions_and_desc, can_restrict_doctypes, restriction_label, doctypes_to_default_filename, max_files_for_doctype, print_outside_form_tag, display_hidden_files, protect_hidden_files, @@ -543,7 +586,9 @@ def create_file_upload_interface(recid, ## Get and clean parameters received from user (file_action, file_target, file_target_doctype, keep_previous_files, file_description, file_comment, file_rename, - file_doctype, file_restriction, uploaded_filename, uploaded_filepath) = \ + file_doctype, file_restriction, uploaded_filename, uploaded_filepath, + copyright_holder, copyright_date, copyright_message, + copyright_holder_contact, license, license_url, license_body) = \ wash_form_parameters(form, abstract_bibdocs, can_keep_doctypes, keep_default, can_describe_doctypes, can_comment_doctypes, can_rename_doctypes, can_name_new_files, can_restrict_doctypes, @@ -599,7 +644,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 +695,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 @@ -679,6 +729,12 @@ def create_file_upload_interface(recid, file_doctype, keep_previous_files, file_restriction, create_related_formats and defer_related_formats_creation) + # Every time we log add action, we need to also log copyright/license change action + log_copyright_action(working_dir, "copyrightChange", + filename, copyright_holder, + copyright_date, copyright_message, + copyright_holder_contact, license, + license_url, license_body) # Automatically create additional formats when # possible AND wanted @@ -702,6 +758,12 @@ def create_file_upload_interface(recid, file_target_doctype, keep_previous_files, file_restriction, create_related_formats and defer_related_formats_creation) + # Every time we log revise action, we need to also log copyright/license change action + log_copyright_action(working_dir, "copyrightChange", + file_target, copyright_holder, + copyright_date, copyright_message, + copyright_holder_contact, license, + license_url, license_body) # Automatically create additional formats when # possible AND wanted additional_formats = [] @@ -747,11 +809,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, @@ -759,6 +821,12 @@ def create_file_upload_interface(recid, file_description, file_comment, file_target_doctype, keep_previous_files, file_restriction) + # Every time we log revise action, we need to also log copyright/license change action + log_copyright_action(working_dir, "copyrightChange", + file_target, copyright_holder, + copyright_date, copyright_message, + copyright_holder_contact, license, + license_url, license_body) elif file_action == "delete" and file_target != "" and \ @@ -794,6 +862,8 @@ def create_file_upload_interface(recid, js_can_describe_doctypes = repr({}.fromkeys(can_describe_doctypes, '')) js_can_comment_doctypes = repr({}.fromkeys(can_comment_doctypes, '')) js_can_restrict_doctypes = repr({}.fromkeys(can_restrict_doctypes, '')) + js_can_change_copyright_doctypes = repr({}.fromkeys(can_change_copyright_doctypes, '')) + js_can_change_advanced_copyright_doctypes = repr({}.fromkeys(can_change_advanced_copyright_doctypes, '')) # Prepare to display file revise panel "balloon". Check if we # should display the list of doctypes or if it is not necessary (0 @@ -808,7 +878,7 @@ def create_file_upload_interface(recid, if bibdoc['get_type'] == doctype]))] doctypes_list = "" if len(cleaned_doctypes) > 1: - doctypes_list = '' + \ '\n'.join(['' \ for (doctype, description) \ @@ -841,6 +911,23 @@ def create_file_upload_interface(recid, else: restrictions_list = '' + # Load copyright mappings from kb and populate the select list + copyright_items = get_kbr_items("Copyrights") + copyrights_list = "" + copyrights_list += '' + + # Save the date of the record in JavaScript so it will accessible later + try: + record_date = get_record(recid).get('imprint','').get('date','') + except AttributeError: + record_date = '' + body += """""" % record_date + # List the files body += '''
@@ -852,6 +939,8 @@ def create_file_upload_interface(recid, body += create_file_row(bibdoc, can_delete_doctypes, can_rename_doctypes, can_revise_doctypes, + can_change_copyright_doctypes, + can_change_advanced_copyright_doctypes, can_describe_doctypes, can_comment_doctypes, can_keep_doctypes, @@ -859,21 +948,24 @@ def create_file_upload_interface(recid, doctypes_list, show_links, can_restrict_doctypes, + copyright_items, even=not (i % 2), ln=ln, form_url_params=form_url_params, protect_hidden_files=protect_hidden_files) body += '' if len(cleaned_doctypes) > 0: - (revise_panel, javascript_prefix) = javascript_display_revise_panel(action='add', target='', show_doctypes=True, show_keep_previous_versions=False, show_rename=can_name_new_files, show_description=True, show_comment=True, bibdocname='', description='', comment='', show_restrictions=True, restriction=len(restrictions_and_desc) > 0 and restrictions_and_desc[0][0] or '', doctypes=doctypes_list) - body += '''%(javascript_prefix)s''' % \ + (revise_panel, javascript_prefix) = javascript_display_revise_panel(action='add', target='', show_doctypes=True, show_keep_previous_versions=False, show_rename=can_name_new_files, show_description=True, show_comment=True, show_copyright=True, show_advanced_copyright=True, bibdocname='', description='', comment='', copyright='', copyright_holder='', copyright_date='', copyright_message='', copyright_holder_contact='', license='', license_url='', license_body='', show_restrictions=True, restriction=len(restrictions_and_desc) > 0 and restrictions_and_desc[0][0] or '', doctypes=doctypes_list) + body += '''%(javascript_prefix)s''' % \ {'display_revise_panel': revise_panel, 'javascript_prefix': javascript_prefix, 'defaultSelectedDoctype': escape_javascript_string(cleaned_doctypes[0], escape_quote_for_html=True), 'add_new_file': _("Add new file"), 'can_describe_doctypes':js_can_describe_doctypes, 'can_comment_doctypes': repr({}.fromkeys(can_comment_doctypes, '')), - 'can_restrict_doctypes': repr({}.fromkeys(can_restrict_doctypes, ''))} + 'can_restrict_doctypes': repr({}.fromkeys(can_restrict_doctypes, '')), + 'can_change_copyright_doctypes': repr({}.fromkeys(can_change_copyright_doctypes, '')), + 'can_change_advanced_copyright_doctypes': repr({}.fromkeys(can_change_advanced_copyright_doctypes, ''))} body += '
' @@ -885,8 +977,8 @@ def create_file_upload_interface(recid, get_upload_file_interface_css() + \ body - # Display markup of the revision panel. This one is also - # printed only at the beginning, so that it does not need to + # Display markup of the revision and copyright panels. Those ones are also + # printed only at the beginning, so that they do not need to # be returned with each response body += revise_balloon % \ {'CFG_SITE_URL': CFG_SITE_URL, @@ -894,6 +986,9 @@ def create_file_upload_interface(recid, 'filename_label': filename_label, 'description_label': description_label, 'comment_label': comment_label, + 'copyright_label': copyright_label, + 'copyrights': copyrights_list, + 'advanced_copyright_label': advanced_copyright_label, 'restrictions': restrictions_list, 'previous_versions_help': _('You can decide to hide or not previous version(s) of this file.').replace("'", "\\'"), 'revise_format_help': _('When you revise a file, the additional formats that you might have previously uploaded are removed, since they no longer up-to-date with the new file.').replace("'", "\\'"), @@ -904,6 +999,10 @@ def create_file_upload_interface(recid, 'uploading_label': _('Uploading...'), 'postprocess_label': _('Please wait...'), 'submit_or_button': form_url_params and 'button' or 'submit'} + body += copyright_balloon % \ + { + 'close': _('Close') + } body += ''' @@ -942,9 +1041,12 @@ def create_file_upload_interface(recid, def create_file_row(abstract_bibdoc, can_delete_doctypes, can_rename_doctypes, can_revise_doctypes, + can_change_copyright_doctypes, + can_change_advanced_copyright_doctypes, can_describe_doctypes, can_comment_doctypes, can_keep_doctypes, can_add_format_to_doctypes, doctypes_list, show_links, can_restrict_doctypes, + copyright_items, even=False, ln=CFG_SITE_LANG, form_url_params='', protect_hidden_files=True): """ @@ -962,6 +1064,12 @@ def create_file_row(abstract_bibdoc, can_delete_doctypes, @param can_revise_doctypes: the list of doctypes that users are allowed to revise. + @param can_change_copyright_doctypes: the list of doctypes for which users + are allowed to select copyright and license. + + @param can_change_advanced_copyright_doctypes: the list of doctypes for which users + are allowed to manually change copyright and license. + @param can_describe_doctypes: the list of doctypes that users are allowed to describe. @@ -981,6 +1089,9 @@ def create_file_row(abstract_bibdoc, can_delete_doctypes, @param show_links: if we display links to files + @param copyright_items: dictionary taken from kb with mappings between + different types of copyrights and copyright data + @param even: if the row is even or odd on the list @type even: boolean @@ -1039,6 +1150,46 @@ def create_file_row(abstract_bibdoc, can_delete_doctypes, out += '' if main_bibdocfile.get_type() in can_revise_doctypes or \ '*' in can_revise_doctypes and not (hidden_p and protect_hidden_files): + # Advanced copyright panel will be inside revise panel, so we need to + # prepare parameters for copyright here + + # Copyright and license link + copyright_license = get_copyright_and_license(abstract_bibdoc['list_latest_files'][0]) + # take the first bibdocfile from list_latest_files, because the + # copyright and license for every bibdocfile should be the same + # I don't think we need to check this condition + # if main_bibdocfile.get_type() in can_change_copyright_doctypes or \ + # '*' in can_change_copyright_doctypes and not (hidden_p and protect_hidden_files): + copyright_holder = copyright_license.get('copyright_holder', '') + copyright_date = copyright_license.get('copyright_date', '') + copyright_message = copyright_license.get('copyright_message', '') + copyright_holder_contact = copyright_license.get('copyright_holder_contact', '') + license = copyright_license.get('license', '') + license_url = copyright_license.get('license_url', '') + license_body = copyright_license.get('license_body', '') + + # Check which copyright/license option should be selected in select list + if copyright_holder or copyright_date or copyright_message or \ + copyright_holder_contact or license or license_url or license_body: + # If at least one copyright/license information is provided it means that the copyright is not the same as record + for copyright in copyright_items: + copyright_value = json.loads(copyright.get('value')).get('Copyright') + license_value = json.loads(copyright.get('value')).get('License') + if copyright_value.get('Holder') == copyright_holder and \ + copyright_value.get('Message') == copyright_message and \ + copyright_value.get('Contact') == copyright_holder_contact and \ + license_value.get('License') == license and \ + license_value.get('Url') == license_url and \ + license_value.get('Body') == license_body: + copyright = copyright.get('key') + break + else: + # None of the predefined value - someone manually set the copyrights + copyright = 'Other' + else: + # There were no copyrights for this file so far + copyright = '' + (revise_panel, javascript_prefix) = javascript_display_revise_panel( action='revise', target=abstract_bibdoc['get_docname'], @@ -1047,9 +1198,19 @@ def create_file_row(abstract_bibdoc, can_delete_doctypes, show_rename=(main_bibdocfile.get_type() in can_rename_doctypes) or '*' in can_rename_doctypes, show_description=(main_bibdocfile.get_type() in can_describe_doctypes) or '*' in can_describe_doctypes, show_comment=(main_bibdocfile.get_type() in can_comment_doctypes) or '*' in can_comment_doctypes, + show_copyright=(main_bibdocfile.get_type() in can_change_copyright_doctypes) or '*' in can_change_copyright_doctypes, + show_advanced_copyright=(main_bibdocfile.get_type() in can_change_advanced_copyright_doctypes) or '*' in can_change_advanced_copyright_doctypes, bibdocname=abstract_bibdoc['get_docname'], description=description, comment=comment, + copyright=copyright, + copyright_holder=copyright_holder, + copyright_date=copyright_date, + copyright_message=copyright_message, + copyright_holder_contact=copyright_holder_contact, + license=license, + license_url=license_url, + license_body=license_body, show_restrictions=(main_bibdocfile.get_type() in can_restrict_doctypes) or '*' in can_restrict_doctypes, restriction=restriction, doctypes=doctypes_list) @@ -1106,9 +1267,19 @@ def create_file_row(abstract_bibdoc, can_delete_doctypes, show_rename=False, show_description=False, show_comment=False, + show_copyright=False, + show_advanced_copyright=False, bibdocname='', description='', comment='', + copyright='', + copyright_holder='', + copyright_date='', + copyright_message='', + copyright_holder_contact='', + license='', + license_url='', + license_body='', show_restrictions=False, restriction=restriction, doctypes=doctypes_list) @@ -1172,6 +1343,15 @@ def build_updated_files_list(bibdocs, actions, recid, display_hidden_files=False for action, bibdoc_name, file_path, rename, description, \ comment, doctype, keep_previous_versions, \ file_restriction, create_related_formats in actions: + # Hack, rename parameters if the type of the action is copyrightChange + # so it's easier to track what is stored in which variable + # We can do this because each action has the same number of parameters + if action == "copyrightChange": + (copyright_holder, copyright_date, + copyright_message, copyright_holder_contact, + license, license_url, license_body) = (file_path, + rename, description, comment, doctype, + keep_previous_versions, file_restriction) dirname, filename, fileformat = decompose_file(file_path) i += 1 if action in ["add", "revise"] and \ @@ -1242,6 +1422,13 @@ def build_updated_files_list(bibdocs, actions, recid, display_hidden_files=False docid=-1, status='', checksum=checksum, more_info=more_info)) abstract_bibdocs[bibdoc_name]['updated'] = True + elif action == "copyrightChange": + copyright = pack_copyrights(copyright_holder, copyright_date, + copyright_message, copyright_holder_contact) + license = pack_license(license, license_url, license_body) + set_copyright_and_license(abstract_bibdocs[bibdoc_name]['list_latest_files'], + copyright, license) + abstract_bibdocs[bibdoc_name]['updated'] = True # For each BibDoc for which we would like to create related # formats, do build the list of formats that should be created @@ -1291,6 +1478,10 @@ def log_action(log_dir, action, bibdoc_name, file_path, rename, format was motivated by the need to have it easily readable by other scripts. Not sure it still makes sense nowadays... + If we no longer need to escape the '---', maybe we can change those log + functions into something more generic like a log function + that records any arbitrary number of parameters ? + Newlines are also reserved, and are escaped from the input values (necessary for the 'comment' field, which is the only one allowing newlines from the browser) @@ -1335,7 +1526,7 @@ def log_action(log_dir, action, bibdoc_name, file_path, rename, try: file_desc = open(log_file, "a+") # We must escape new lines from comments in some way: - comment = str(comment).replace('\\', '\\\\').replace('\r\n', '\\n\\r') + comment = str(comment).replace('\\', '\\\\').replace('\r\n', '\\n\\r').replace('\n', '\\n') msg = action + '<--->' + \ bibdoc_name.replace('---', '___') + '<--->' + \ file_path + '<--->' + \ @@ -1351,10 +1542,64 @@ def log_action(log_dir, action, bibdoc_name, file_path, rename, except Exception ,e: raise e +def log_copyright_action(log_dir, action, bibdoc_name, copyright_holder, + copyright_date, copyright_message, + copyright_holder_contact, license, license_url, + license_body): + """ + Logs copyright change action in the actions log. + See log_action() function for more information + @param log_dir: directory where to save the log (ie. working_dir) + + @param action: the performed action (one of 'revise', 'delete', + 'add', 'addFormat') + + @param bibdoc_name: the name of the bibdoc on which the change is + applied + + @param copyright_holder: holder of the copyright associated with the file + + @param copyright_date: date of the copyright associated with the file + + @param copyright_message: message associated with the copyright + + @param copyright_holder_contact: contact information of the copyright holder + + @param license: license of the file + + @param license_url: url of the license related to the file + + @param license_body: person or institution imposing the license + (author, publisher) + + """ + log_file = os.path.join(log_dir, 'bibdocactions.log') + try: + file_desc = open(log_file, "a+") + # We must escape new lines from copyright message in some way: + copyright_message = str(copyright_message).replace('\\', '\\\\').replace('\r\n', '\\n\\r').replace('\n', '\\n') + msg = action + '<--->' + \ + bibdoc_name.replace('---', '___') + '<--->' + \ + copyright_holder + '<--->' + \ + copyright_date + '<--->' + \ + copyright_message + '<--->' + \ + copyright_holder_contact + '<--->' + \ + license + '<--->' + \ + license_url + '<--->' + \ + license_body + '<--->' + \ + '\n' # This last <---> is to match the number of parameters of log_action function + file_desc.write("%s --> %s" %(time.strftime("%Y-%m-%d %H:%M:%S"), msg)) + file_desc.close() + except Exception, e: + raise e + + def read_actions_log(log_dir): """ Reads the logs of action to be performed on files + Both log_copyright_action and log_action create rows with the same number + of parameters, so each action can be treated in the same way. See log_action(..) for more information about the structure of the log file. @@ -1368,42 +1613,62 @@ def read_actions_log(log_dir): file_desc = open(log_file, "r") for line in file_desc.readlines(): (timestamp, action) = line.split(' --> ', 1) - try: - (action, bibdoc_name, file_path, rename, description, - comment, doctype, keep_previous_versions, - file_restriction, create_related_formats) = action.rstrip('\n').split('<--->') - except ValueError, e: - # Malformed action log - pass - - # Clean newline-escaped comment: - comment = comment.replace('\\n\\r', '\r\n').replace('\\\\', '\\') - - # Perform some checking - if action not in CFG_ALLOWED_ACTIONS: - # Malformed action log - pass - - try: - keep_previous_versions = int(keep_previous_versions) - except: - # Malformed action log - keep_previous_versions = 1 - pass - - create_related_formats = create_related_formats == 'True' and True or False - - actions.append((action, bibdoc_name, file_path, rename, \ - description, comment, doctype, - keep_previous_versions, file_restriction, - create_related_formats)) + if action.split('<--->', 1)[0] == 'copyrightChange': + try: + # if the action is "copyrightChange" we want to name + # parameters differently for clarity + (action, file_path, copyright_holder, copyright_date, copyright_message, + copyright_holder_contact, license, license_url, + license_body, _) = action.rstrip('\n').split('<--->') + except ValueError, e: + # Malformed action log + pass + # Clean newline-escaped copyright_message: + copyright_message = copyright_message.replace('\\n\\r', '\r\n').replace('\\\\', '\\').replace('\\n', '\n') + # Perform some checking + if action not in CFG_ALLOWED_ACTIONS: + # Malformed action log + pass + actions.append((action, file_path, copyright_holder, copyright_date, + copyright_message, copyright_holder_contact, + license, license_url, license_body, '')) + else: + try: + (action, bibdoc_name, file_path, rename, description, + comment, doctype, keep_previous_versions, + file_restriction, create_related_formats) = action.rstrip('\n').split('<--->') + except ValueError, e: + # Malformed action log + pass + # Clean newline-escaped comment: + comment = comment.replace('\\n\\r', '\r\n').replace('\\\\', '\\').replace('\\n', '\n') + try: + keep_previous_versions = int(keep_previous_versions) + except: + # Malformed action log + keep_previous_versions = 1 + # Perform some checking + if action not in CFG_ALLOWED_ACTIONS: + # Malformed action log + pass + actions.append((action, bibdoc_name, file_path, rename, + description, comment, doctype, + keep_previous_versions, file_restriction, + create_related_formats)) file_desc.close() except: pass return actions -def javascript_display_revise_panel(action, target, show_doctypes, show_keep_previous_versions, show_rename, show_description, show_comment, bibdocname, description, comment, show_restrictions, restriction, doctypes): +def javascript_display_revise_panel(action, target, show_doctypes, + show_keep_previous_versions, show_rename, + show_description, show_comment, show_copyright, + show_advanced_copyright, bibdocname, description, + comment, copyright, copyright_holder, copyright_date, + copyright_message, copyright_holder_contact, + license, license_url, license_body, + show_restrictions, restriction, doctypes): """ Returns a correctly encoded call to the javascript function to display the revision panel. @@ -1420,9 +1685,19 @@ def javascript_display_revise_panel(action, target, show_doctypes, show_keep_pre "showRename": %(showRename)s, "showDescription": %(showDescription)s, "showComment": %(showComment)s, + "showCopyright": %(showCopyright)s, + "showAdvancedCopyright": %(showAdvancedCopyright)s, "bibdocname": "%(bibdocname)s", "description": "%(description)s", "comment": "%(comment)s", + "copyright": "%(copyright)s", + "copyrightHolder": "%(copyrightHolder)s", + "copyrightDate": "%(copyrightDate)s", + "copyrightMessage": "%(copyrightMessage)s", + "copyrightHolderContact": "%(copyrightHolderContact)s", + "license": "%(license)s", + "licenseUrl": "%(licenseUrl)s", + "licenseBody": "%(licenseBody)s", "showRestrictions": %(showRestrictions)s, "restriction": "%(restriction)s", "doctypes": "%(doctypes)s"} @@ -1436,8 +1711,18 @@ def javascript_display_revise_panel(action, target, show_doctypes, show_keep_pre 'showKeepPreviousVersions': show_keep_previous_versions and 'true' or 'false', 'showComment': show_comment and 'true' or 'false', 'showDescription': show_description and 'true' or 'false', + 'showCopyright': show_copyright and 'true' or 'false', + 'showAdvancedCopyright': show_advanced_copyright and 'true' or 'false', 'description': description and escape_javascript_string(description, escape_for_html=False) or '', 'comment': comment and escape_javascript_string(comment, escape_for_html=False) or '', + 'copyright': copyright and escape_javascript_string(copyright, escape_for_html=False) or '', + 'copyrightHolder': copyright_holder and escape_javascript_string(copyright_holder, escape_for_html=False) or '', + 'copyrightDate': copyright_date and escape_javascript_string(copyright_date, escape_for_html=False) or '', + 'copyrightMessage': copyright_message and escape_javascript_string(copyright_message, escape_for_html=False) or '', + 'copyrightHolderContact': copyright_holder_contact and escape_javascript_string(copyright_holder_contact, escape_for_html=False) or '', + 'license': license and escape_javascript_string(license, escape_for_html=False) or '', + 'licenseUrl': license_url and escape_javascript_string(license_url, escape_for_html=False) or '', + 'licenseBody': license_body and escape_javascript_string(license_body, escape_for_html=False) or '', 'showRestrictions': show_restrictions and 'true' or 'false', 'restriction': escape_javascript_string(restriction, escape_for_html=False), 'doctypes': escape_javascript_string(doctypes, escape_for_html=False)} @@ -1457,8 +1742,9 @@ def get_uploaded_files_for_docname(log_dir, docname): """ return [file_path for action, bibdoc_name, file_path, rename, \ description, comment, doctype, keep_previous_versions , \ - file_restriction, create_related_formats in read_actions_log(log_dir) \ - if bibdoc_name == docname and os.path.exists(file_path)] + file_restriction, copyright_license in read_actions_log(log_dir) \ + if action in ['revise', 'add', 'addFormat'] and \ + bibdoc_name == docname and os.path.exists(file_path)] def get_bibdoc_for_docname(docname, abstract_bibdocs): """ @@ -1549,6 +1835,14 @@ def get_description_and_comment(bibdocfiles): return (description, comment) +def get_copyright_and_license(bibdocfile): + """ + Returns the copyright and license of a BibDoc as a dictionary + """ + copyright_license = bibdocfile.get_copyright() + copyright_license.update(bibdocfile.get_license()) + return copyright_license + def set_description_and_comment(abstract_bibdocfiles, description, comment): """ Set the description and comment to the given (abstract) @@ -1571,6 +1865,67 @@ def set_description_and_comment(abstract_bibdocfiles, description, comment): bibdocfile.description = description bibdocfile.comment = comment +def set_copyright_and_license(abstract_bibdocfiles, copyright, license): + """ + Set the copyright and license to the given (abstract) + bibdocfiles. + + @param abstract_bibdocfiles: the list of 'abstract' files of a + given bibdoc for which we want to set the + copyright and license. + + @param copyright: the new copyright + @param license: the new license + """ + for bibdocfile in abstract_bibdocfiles: + bibdocfile.copyright = copyright + bibdocfile.license = license + +def pack_copyrights(copyright_holder, copyright_date, copyright_message, + copyright_holder_contact): + """ + Packs the parameters into copyright dictionary with properly named keys. + Since we pass those copyright values in a few places, we pack them + here, so when the keys change, we won't have to rename them everywhere. + + @param copyright_holder: holder of the copyright associated with the file + @type copyright_holder: string + @param copyright_date: date of the copyright associated with the file + @type copyright_date: string + @param copyright_message: message associated with the copyright + @type copyright_message: string + @param copyright_holder_contact: contact information of the copyright holder + @type copyright_holder_contact: string + """ + copyright = {} + copyright['copyright_holder'] = copyright_holder + copyright['copyright_date'] = copyright_date + copyright['copyright_message'] = copyright_message + copyright['copyright_holder_contact'] = copyright_holder_contact + + return copyright + +def pack_license(license, license_url, license_body): + """ + Packs the parameters into license dictionary with properly named keys. + Since we pass those license values in a couple of places, we pack them + here, so when the keys change, we won't have to replace them everywhere + + @param license: license of the file + @type license: string + @param license_url: url of the license related to the file + @type license_url: string + @param license_body: person or institution imposing the license + (author, publisher) + @type license_body: string + """ + packed_license = {} + packed_license['license'] = license + packed_license['license_url'] = license_url + packed_license['license_body'] = license_body + + return packed_license + def delete_file(working_dir, file_path): """ Deletes a file at given path from the file. @@ -1641,7 +1996,9 @@ def wash_form_parameters(form, abstract_bibdocs, can_keep_doctypes, @return: tuple (file_action, file_target, file_target_doctype, keep_previous_files, file_description, file_comment, - file_rename, file_doctype, file_restriction) where:: + file_rename, file_doctype, file_restriction, copyright_holder, + copyright_date, copyright_message, copyright_holder_contact, + license, license_url, license_body) where:: file_action: *str* the performed action ('add', 'revise','addFormat' or 'delete') @@ -1685,14 +2042,32 @@ def wash_form_parameters(form, abstract_bibdocs, can_keep_doctypes, file_path: *str* the full path to the file + copyright_holder: *str* holder of the copyright of the file + + copyright_date: *str* date of the copyright + + copyright_message: *str* message of the copyright + + copyright_holder_contact: *str* contact information of the copyright + holder + + license: *str* license of the file + + license_url: *str* url of the license related to the file + + license_body: *str* person or institution imposing the license + (author, publisher) + @rtype: tuple(string, string, string, boolean, string, string, - string, string, string, string, string) + string, string, string, string, string, string, + string, string, string, string, string, string) """ # Action performed ... if form.has_key("fileAction") and \ form['fileAction'] in CFG_ALLOWED_ACTIONS: file_action = str(form['fileAction']) # "add", "revise", - # "addFormat" or "delete" + # "addFormat" "copyrightChange" + # or "delete" else: file_action = "" @@ -1842,10 +2217,20 @@ def wash_form_parameters(form, abstract_bibdocs, can_keep_doctypes, file_name = None file_path = None + # Escape fields related to copyright and license + copyright_holder = str(form.get('copyrightHolder','')) + copyright_date = str(form.get('copyrightDate','')) + copyright_message = str(form.get('copyrightMessage','')) + copyright_holder_contact = str(form.get('copyrightHolderContact','')) + license = str(form.get('license','')) + license_url = str(form.get('licenseUrl','')) + license_body = str(form.get('licenseBody','')) + return (file_action, file_target, file_target_doctype, keep_previous_files, file_description, file_comment, file_rename, file_doctype, file_restriction, file_name, - file_path) + file_path, copyright_holder, copyright_date, copyright_message, + copyright_holder_contact, license, license_url, license_body) def move_uploaded_files_to_storage(working_dir, recid, icon_sizes, @@ -1934,8 +2319,6 @@ def move_uploaded_files_to_storage(working_dir, recid, icon_sizes, if new_bibdoc: newly_added_bibdocs.append(new_bibdoc) - - if create_related_formats: # Schedule creation of related formats create_related_formats_for_bibdocs[rename or bibdoc_name] = True @@ -1947,13 +2330,22 @@ def move_uploaded_files_to_storage(working_dir, recid, icon_sizes, elif action == 'delete': delete(bibdoc_name, recid, working_dir, pending_bibdocs, bibrecdocs) + elif action == 'copyrightChange': + # Don't worry about those strange variable names that are being + # send to those two functions below. Those variables store proper + # copyright and license data, there is just no point in renaming + # them only to pass them as arguments to the functions + copyright = pack_copyrights(file_path, rename, description, comment) + license = pack_license(doctype, keep_previous_versions, file_restriction) + copyright_license_change(file_path, bibdoc_name, copyright, + license, recid, working_dir, bibrecdocs) # Finally rename bibdocs that should be named according to a file in # curdir (eg. naming according to report number). Only consider # file that have just been added. parameters = _read_file_revision_interface_configuration_from_disk(working_dir) new_names = [] - doctypes_to_default_filename = parameters[22] + doctypes_to_default_filename = parameters[26] for bibdoc_to_rename in newly_added_bibdocs: bibdoc_to_rename_doctype = bibdoc_to_rename.doctype rename_to = doctypes_to_default_filename.get(bibdoc_to_rename_doctype, '') @@ -2115,6 +2507,28 @@ def add_format(file_path, bibdoc_name, recid, doctype, working_dir, 'named %s in record %i.' % \ (file_path, bibdoc_name, recid), alert_admin=True) +def copyright_license_change(file_path, bibdoc_name, copyright, license, recid, working_dir, bibrecdocs): + """ + Changes the copyright and license for the given bibdoc + """ + added_bibdoc = None + try: + brd = BibRecDocs(recid) + bibdoc = bibrecdocs.get_bibdoc(bibdoc_name) + bibdoc.set_copyright(copyright) + bibdoc.set_license(license) + _do_log(working_dir, 'Changed copyright and license of ' + \ + brd.get_docname(bibdoc.id) + ': ' + ', '.join(copyright.values() + license.values())) + + except InvenioBibDocFileError, e: + # Something went wrong, let's report it ! + register_exception(prefix='Move_Uploaded_Files_to_Storage ' \ + 'tried to change copyright and license of a file %s ' \ + 'named %s in record %i.' % \ + (file_path, bibdoc_name, recid), + alert_admin=True) + + return added_bibdoc def revise(file_path, bibdoc_name, rename, doctype, description, comment, file_restriction, icon_sizes, create_icon_doctypes, @@ -2411,14 +2825,14 @@ def get_upload_file_interface_javascript(form_url_params): $('#bibdocfilemanagedocfileuploadbutton').click(function() { this_form.bibdocfilemanagedocfileuploadbuttonpressed=true; this_form.ajaxSubmit(options); - }) + }); }); // post-submit callback function showResponse(responseText, statusText) { hide_upload_progress(); - hide_revise_panel(); + hide_panels(); } ''' % { 'form_url_params': form_url_params, @@ -2439,14 +2853,24 @@ def get_upload_file_interface_javascript(form_url_params): var showRename = params['showRename']; var showDescription = params['showDescription']; var showComment = params['showComment']; + var showCopyright = params['showCopyright']; + var showAdvancedCopyright = params['showAdvancedCopyright']; var bibdocname = params['bibdocname']; var description = params['description']; var comment = params['comment']; + var copyright = params['copyright']; + var copyrightHolder = params['copyrightHolder']; + var copyrightDate = params['copyrightDate']; + var copyrightMessage = params['copyrightMessage']; + var copyrightHolderContact = params['copyrightHolderContact']; + var license = params['license']; + var licenseUrl = params['licenseUrl']; + var licenseBody = params['licenseBody']; var showRestrictions = params['showRestrictions']; var restriction = params['restriction']; var doctypes = params['doctypes']; - var balloon = document.getElementById("balloon"); + var balloon = document.getElementById("reviseBalloon"); var file_input_block = document.getElementById("balloonReviseFileInputBlock"); var doctype = document.getElementById("fileDoctypesRow"); var warningFormats = document.getElementById("warningFormats"); @@ -2454,6 +2878,10 @@ def get_upload_file_interface_javascript(form_url_params): var renameBox = document.getElementById("renameBox"); var descriptionBox = document.getElementById("descriptionBox"); var commentBox = document.getElementById("commentBox"); + var copyrightBox = document.getElementById("copyrightBox"); + var copyrightSelectList = document.getElementById("copyright"); + var advancedCopyrightLinkBox = document.getElementById("advancedCopyrightLink"); + var advancedCopyrightLink = document.getElementById("advancedCopyrightLicense"); var restrictionBox = document.getElementById("restrictionBox"); var apply_button = document.getElementById("applyChanges"); var mainForm = getMainForm(); @@ -2486,6 +2914,16 @@ def get_upload_file_interface_javascript(form_url_params): } else { commentBox.style.display = 'none' } + if ((action == 'revise' || action == 'add') && showCopyright == true){ + copyrightBox.style.display = '' + } else { + copyrightBox.style.display = 'none' + } + if ((action == 'revise' || action == 'add') && showAdvancedCopyright == true){ + advancedCopyrightLinkBox.style.display = 'inline' + } else { + advancedCopyrightLinkBox.style.display = 'none' + } if ((action == 'revise' || action == 'add') && showRestrictions == true){ restrictionBox.style.display = '' } else { @@ -2506,6 +2944,13 @@ def get_upload_file_interface_javascript(form_url_params): mainForm.rename.value = bibdocname; mainForm.comment.value = comment; mainForm.description.value = description; + mainForm.copyrightHolder.value = copyrightHolder; + mainForm.copyrightDate.value = copyrightDate; + mainForm.copyrightMessage.value = copyrightMessage; + mainForm.copyrightHolderContact.value = copyrightHolderContact; + mainForm.license.value = license; + mainForm.licenseUrl.value = licenseUrl; + mainForm.licenseBody.value = licenseBody; var fileRestrictionFound = false; for (var i=0; i < mainForm.fileRestriction.length; i++) { if (mainForm.fileRestriction[i].value == restriction) { @@ -2520,6 +2965,9 @@ def get_upload_file_interface_javascript(form_url_params): mainForm.fileRestriction.selectedIndex = lastIndex; } + /* Set the correct copyright option in select box*/ + copyrightSelectList.value = copyright + /* Display and move to correct position*/ pos = findPosition(link) balloon.style.display = ''; @@ -2537,20 +2985,114 @@ def get_upload_file_interface_javascript(form_url_params): if (apply_button) { apply_button.disabled = true; } + + // Unbind previous binding - otherwise we will get as many update_copyright_fields() + //calls as many time we have clicked the "revise" link + $('#copyright').off("change"); + // Bind: changing the select option from copyrights list will update advanced copyrights fields ... + $('#copyright').on("change", function(){ + var val = $('#copyright option:selected').val(); + update_copyright_fields(val, params); + }); + + /* ... and trigger it for the first time, so the advanced fields get filled with data */ + $('#copyright').trigger('change'); + + /* Display advanced copyright/license panel*/ + $(advancedCopyrightLink).click(function(event){ + link = event.target; + display_copyright_panel(link, params); + return false; + }); + /*gray_out(true);*/ } -function hide_revise_panel(){ - var balloon = document.getElementById("balloon"); + +function display_copyright_panel(link, params){ + /* Just display here, update takes place in a different function */ + + var balloon = document.getElementById("copyrightBalloon"); + var pos; + + /* Display and move to correct position*/ + pos = findPosition(link) + balloon.style.display = ''; + balloon.style.position="absolute"; + balloon.style.left = pos[0] + link.offsetWidth +"px"; + balloon.style.top = pos[1] - Math.round(balloon.offsetHeight/2) + 5 + "px"; + balloon.style.zIndex = 1001; + balloon.style.display = ''; +} + +function hide_panels(){ + var reviseBalloon = document.getElementById("reviseBalloon"); + var copyrightBalloon = document.getElementById("copyrightBalloon"); var apply_button = document.getElementById("applyChanges"); - balloon.style.display = 'none'; - if (apply_button) { - apply_button.disabled = false; + if (copyrightBalloon.style.display != 'none') { + copyrightBalloon.style.display = 'none'; + } else if (reviseBalloon.style.display != 'none') { + reviseBalloon.style.display = 'none'; + if (apply_button) { + apply_button.disabled = false; + } } /*gray_out(false);*/ } +function update_copyright_fields(item, params){ + var copyrightHolderField = document.getElementById("copyrightHolder"); + var copyrightDateField = document.getElementById("copyrightDate"); + var copyrightMessageField = document.getElementById("copyrightMessage"); + var copyrightHolderContactField = document.getElementById("copyrightHolderContact"); + var licenseField = document.getElementById("license"); + var licenseUrlField = document.getElementById("licenseUrl"); + var licenseBodyField = document.getElementById("licenseBody"); + + if (!item) { + /* In case item is empty (the same copyrights for a file as for a record) we want to clear all field anything */ + copyrightHolderField.value = ''; + copyrightDateField.value = ''; + copyrightMessageField.value = ''; + copyrightHolderContactField.value = ''; + licenseField.value = ''; + licenseUrlField.value = ''; + licenseBodyField.value = ''; + } else { + /* In case item is "Other" try to load initial values (those that + were there when to balloon opened) or don't change anything */ + if (item == "Other") { + /* Copyrights will be manually edited by user */ + copyrightHolderField.value = params['copyrightHolder']; + copyrightDateField.value = params['copyrightDate']; + copyrightMessageField.value = params['copyrightMessage']; + copyrightHolderContactField.value = params['copyrightHolderContact']; + licenseField.value = params['license']; + licenseUrlField.value = params['licenseUrl']; + licenseBodyField.value = params['licenseBody']; + } else { + /* One of the options in select box is selected */ + /* Get all mappings for the select item */ + $.ajax({ + url: '%(CFG_SITE_URL)s/kb/export', + dataType: 'json', + type: 'GET', + data: {kbname:"Copyrights", searchkey:item, format: 'kba'}, + success: function(json) { + copyrightHolderField.value = json.Copyright.Holder; + /* If the date was not edited by user, use the date of a record */ + copyrightDateField.value = params['copyrightDate'] || recordDate; + copyrightMessageField.value = json.Copyright.Message; + copyrightHolderContactField.value = json.Copyright.Contact; + licenseField.value = json.License.License; + licenseUrlField.value = json.License.Url; + licenseBodyField.value = json.License.Body; + } + }); + } + } +} -/* Intercept ESC key in order to close revise panel*/ +/* Intercept ESC key in order to close revise or copyright panel*/ document.onkeyup = keycheck; function keycheck(e){ var KeyID = (window.event) ? event.keyCode : e.keyCode; @@ -2559,7 +3101,7 @@ def get_upload_file_interface_javascript(form_url_params): if (upload_in_progress_p) { hide_upload_progress(); } else { - hide_revise_panel(); + hide_panels(); } } } @@ -2634,7 +3176,7 @@ def get_upload_file_interface_javascript(form_url_params): return true; } -function updateForm(doctype, can_describe_doctypes, can_comment_doctypes, can_restrict_doctypes) { +function updateForm(doctype, can_describe_doctypes, can_comment_doctypes, can_restrict_doctypes, can_change_copyright_doctypes, can_change_advanced_copyright_doctypes) { /* Update the revision panel to hide or not part of the interface * based on selected doctype * @@ -2647,11 +3189,14 @@ def get_upload_file_interface_javascript(form_url_params): var renameBox = document.getElementById("renameBox"); var descriptionBox = document.getElementById("descriptionBox"); var commentBox = document.getElementById("commentBox"); + var copyrightBox = document.getElementById("copyrightBox"); var restrictionBox = document.getElementById("restrictionBox"); if (!can_describe_doctypes) {var can_describe_doctypes = [];} if (!can_comment_doctypes) {var can_comment_doctypes = [];} if (!can_restrict_doctypes) {var can_restrict_doctypes = [];} + if (!can_change_copyright_doctypes) {var can_change_copyright_doctypes = [];} + if (!can_change_advanced_copyright_doctypes) {var can_change_advanced_copyright_doctypes = [];} if ((doctype in can_describe_doctypes) || ('*' in can_describe_doctypes)){ @@ -2674,8 +3219,15 @@ def get_upload_file_interface_javascript(form_url_params): restrictionBox.style.display = 'none' } + if ((doctype in can_change_copyright_doctypes) || + ('*' in can_change_copyright_doctypes)){ + copyrightBox.style.display = '' + } else { + copyrightBox.style.display = 'none' + } + /* Move the revise panel accordingly */ - var balloon = document.getElementById("balloon"); + var balloon = document.getElementById("reviseBalloon"); pos = findPosition(last_clicked_link) balloon.style.display = ''; balloon.style.position="absolute"; @@ -2751,7 +3303,8 @@ def get_upload_file_interface_javascript(form_url_params): } --> -''' % {'CFG_SITE_RECORD': CFG_SITE_RECORD} +''' % {'CFG_SITE_RECORD': CFG_SITE_RECORD, + 'CFG_SITE_URL': CFG_SITE_URL} return javascript def get_upload_file_interface_css(): @@ -2828,45 +3381,45 @@ def get_upload_file_interface_css(): } */ -#balloon table{ +.balloon table{ border-collapse:collapse; border-spacing: 0px; } -#balloon table td.topleft{ +.balloon table td.topleft{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_top_left_shadow.png) no-repeat bottom right; } -#balloon table td.bottomleft{ +.balloon table td.bottomleft{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_bottom_left_shadow.png) no-repeat top right; } -#balloon table td.topright{ +.balloon table td.topright{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_top_right_shadow.png) no-repeat bottom left; } -#balloon table td.bottomright{ +.balloon table td.bottomright{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_bottom_right_shadow.png) no-repeat top left; } -#balloon table td.top{ +.balloon table td.top{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_top_shadow.png) repeat-x bottom left; } -#balloon table td.bottom{ +.balloon table td.bottom{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_bottom_shadow.png) repeat-x top left; } -#balloon table td.left{ +.balloon table td.left{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_left_shadow.png) repeat-y top right; text-align:right; padding:0; } -#balloon table td.right{ +.balloon table td.right{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_right_shadow.png) repeat-y top left; } -#balloon table td.arrowleft{ +.balloon table td.arrowleft{ background: transparent url(%(CFG_SITE_URL)s/img/balloon_arrow_left_shadow.png) no-repeat bottom right; width:24px; height:27px; } -#balloon table td.center{ +.balloon table td.center{ background-color:#ffffea; } -#balloon label{ +.balloon label{ font-size:small; } #balloonReviseFile{ @@ -2888,6 +3441,16 @@ def get_upload_file_interface_css(): #description, #comment, #rename { width:90%%; } +label { + display: inline-block; + min-width: 60px; +} +#advancedCopyrightLicense{ + font-size: smaller; +} +#copyrightBox, #restrictionBox{ + margin: 1px; +} .rotatingprogress, .rotatingpostprocess { position:relative; float:right; @@ -2926,7 +3489,7 @@ def get_upload_file_interface_css(): # The HTML markup of the revise panel revise_balloon = ''' -