diff --git a/bower.json b/bower.json index ba7c4c074..f923d569d 100644 --- a/bower.json +++ b/bower.json @@ -1,41 +1,42 @@ { "name": "invenio-grunt", "version": "0.1.0", "dependencies": { "bootstrap": "3.1.1", "jquery": "2.1.0", "font-awesome": "~4.0.3", "ckeditor": "latest" }, "devDependencies": { "typeahead.js": "0.10.1", "hogan": "3.0.0", "jquery-tokeninput": "*", "MathJax": "v2.1-latest", "jquery_jeditable": "http://invenio-software.org/download/jquery/v1.5/js/jquery.jeditable.mini.js", "jquery.treeview": "1.4.1", "jquery.tablesorter": "http://invenio-software.org/download/jquery/jquery.tablesorter.20111208.zip", "jqueryui": "1.10.4", "jquery.ui.timepicker": "http://invenio-software.org/download/jquery/jquery-ui-timepicker-addon-1.0.3.js", "jquery.multifile": "http://jquery-multifile-plugin.googlecode.com/svn/trunk/jquery.MultiFile.pack.js", "jquery.ajaxpager": "http://invenio-software.org/download/jquery/v1.5/js/jquery.ajaxPager.js", "jquery.bookmark": "http://invenio-software.org/download/jquery/jquery.bookmark.package-1.4.0.zip", "DataTables": "1_9", "jquery-flot": "*", "form": "https://raw.github.com/malsup/form/master/jquery.form.js", "jquery.hotkeys": "*", "uploadify": "*", "json2": "*", "prism": "gh-pages", "swfobject": "https://github.com/swfobject/swfobject/blob/master/swfobject/swfobject.js", "datatables-colvis": "*", "typeahead.js-bootstrap3.less": "0.2.2", "DataTables-Plugins": "https://github.com/DataTables/Plugins.git", "plupload": "*", "bootstrap-switch": "https://github.com/nostalgiaz/bootstrap-switch.git#3.0", - "font-awesome": "*" + "font-awesome": "*", + "bootstrap-tagsinput": "*" }, "resolutions": { "jquery": "2.1.0" } } diff --git a/grunt/copy.js b/grunt/copy.js index 099192b7b..2a16875a8 100644 --- a/grunt/copy.js +++ b/grunt/copy.js @@ -1,258 +1,261 @@ /* * This file is part of Invenio. * Copyright (C) 2014 CERN. * * Invenio is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Invenio is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Invenio; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ 'use strict'; /*jshint laxcomma:true */ // FIXME? module.exports = { css: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>', src: ['bootstrap/dist/css/bootstrap*' ,'font-awesome/css/font-awesome.*' ,'jquery-tokeninput/styles/token-input-facebook.css' ,'jquery-tokeninput/styles/token-input.css' ,'jquery.bookmark/jquery.bookmark.css' ,'datatables-colvis/media/css/ColVis.css' ,'DataTables-Plugins/integration/bootstrap/3/dataTables.bootstrap.css' - ,'prism/prism.css'], + ,'prism/prism.css' + ,'bootstrap-tagsinput/dist/bootstrap-tagsinput.css'], dest: '<%= globalConfig.installation_path %>/css/' }, jquery_css: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>', src: ['jqueryui/themes/base/jquery.ui.datepicker.css' ,'jqueryui/themes/base/jquery.ui.theme.css'], dest: '<%= globalConfig.installation_path %>/img/jquery-ui/themes/base/' }, jquery_imgs: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>', src: ['jqueryui/themes/base/images/ui-bg_flat_75_ffffff_40x100.png'], dest: '<%= globalConfig.installation_path %>/img/jquery-ui/themes/base/images/' }, img: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['jquery.bookmark/bookmarks.png' ,'uploadify/uploadify*' ,'!uploadify/uploadify.php' ,'datatables-colvis/media/images/button.png' ,'DataTables-Plugins/integration/bootstrap/3/images/*.png'], dest: '<%= globalConfig.installation_path %>/img/' }, js: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['bootstrap/dist/js/bootstrap.js' ,'bootstrap/dist/js/bootstrap.min.js' ,'jquery/dist/jquery.min.js' ,'jquery/dist/jquery.min.map' ,'jquery-tokeninput/src/jquery.tokeninput.js' ,'jquery.bookmark/jquery.bookmark.min.js' ,'DataTables/media/js/jquery.dataTables.js' ,'jquery-flot/excanvas.min.js' ,'jquery-flot/jquery.flot.js' ,'jquery-flot/jquery.flot.selection.js' ,'jquery.hotkeys/jquery.hotkeys.js' ,'uploadify/jquery.uploadify.min.js' ,'json2/json2.js' ,'datatables-colvis/media/js/ColVis.js' ,'DataTables-Plugins/integration/bootstrap/3/dataTables.bootstrap.js' - ,'prism/prism.js'], + ,'prism/prism.js' + ,'bootstrap-tagsinput/dist/bootstrap-tagsinput.min.js' + ,'bootstrap-tagsinput/dist/bootstrap-tagsinput.min.js.map'], dest: '<%= globalConfig.installation_path %>/js/' }, fonts: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['bootstrap/dist/fonts/glyphicons-halflings-regular.*' ,'font-awesome/fonts/*'], dest: '<%= globalConfig.installation_path %>/fonts/' }, typeahead: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['typeahead.js/dist/typeahead.bundle.min.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'typeahead.js'); return dest + res; } }, typeaheadBootstrap3Css: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/typeahead.js-bootstrap3.less', src: ['typeahead.css'], dest: '<%= globalConfig.installation_path %>/css/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'typeahead.js-bootstrap.css'); return dest + res; } }, hogan: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['hogan/web/builds/2.0.0/hogan-2.0.0.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { return dest + src.substring(0, src.indexOf('-')) + '.js'; } }, jqueryUI: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/jqueryui', src: ['**'], dest: '<%= globalConfig.installation_path %>/js/jqueryui' }, jqueryUISortable: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['jquery.ui/ui/jquery.ui.sortable.js'], dest: '<%= globalConfig.installation_path %>/js/' }, jqueryTimePicker: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['jquery.ui.timepicker/index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'jquery-ui-timepicker-addon.js'); return dest + res; } }, MultiFile: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['jquery.multifile/index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'jquery.MultiFile.pack.js'); return dest + res; } }, ajaxPager: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['jquery.ajaxpager/index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'jquery.ajaxPager.js'); return dest + res; } }, form: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['form/index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'jquery.form.js'); return dest + res; } }, swfobject: { expand: true, flatten: true, cwd: '<%= globalConfig.bower_path %>/', src: ['swfobject/index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { var res = src.replace(src.substring(0), 'swfobject.js'); return dest + res; } }, MathJax: { expand: true, cwd: '<%= globalConfig.bower_path %>/MathJax/', src: ['**'], dest: '<%= globalConfig.installation_path %>/MathJax/' }, ckeditor: { expand: true, cwd: '<%= globalConfig.bower_path %>/ckeditor/', src: ['**', '!**_samples/**', '!**_source/**', '!**php**', '!**_**', '!**pack**', '!**ckeditor.asp'], dest: '<%= globalConfig.installation_path %>/ckeditor/' }, jqueryTreeview: { expand: true, cwd: '<%= globalConfig.bower_path %>/jquery.treeview/', src: ['**'], dest: '<%= globalConfig.installation_path %>/js/jquery-treeview/' }, jqueryTableSorter: { expand: true, cwd: '<%= globalConfig.bower_path %>/jquery.tablesorter/', src: ['**'], dest: '<%= globalConfig.installation_path %>/js/tablesorter/' }, themesUI: { expand: true, cwd: '<%= globalConfig.bower_path %>/jquery.ui/themes/', src: ['**'], dest: '<%= globalConfig.installation_path %>/img/jquery-ui/themes' }, imagesUI: { expand: true, cwd: '<%= globalConfig.bower_path %>/jquery.ui/themes/base/images/', src: ['**'], dest: '<%= globalConfig.installation_path %>/img/images/' }, jeditable: { expand: true, cwd: '<%= globalConfig.bower_path %>/jquery_jeditable/', src: ['index.js'], dest: '<%= globalConfig.installation_path %>/js/', rename: function(dest, src) { if (src === 'index.js') { return dest + 'jquery.jeditable.mini.js'; } return dest + src; } }, plupload: { expand: true, cwd: '<%= globalConfig.bower_path %>/plupload/js', src: ['**'], dest: '<%= globalConfig.installation_path %>/plupload/' } }; diff --git a/invenio/modules/deposit/models.py b/invenio/modules/deposit/models.py index 189ddab69..f8090d7ac 100644 --- a/invenio/modules/deposit/models.py +++ b/invenio/modules/deposit/models.py @@ -1,1500 +1,1499 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ Classes for wrapping BibWorkflowObject and friends to make it easier to work with the data attributes. """ from uuid import uuid4 import copy import json import os from datetime import datetime from dateutil.tz import tzutc from sqlalchemy.orm.exc import NoResultFound from werkzeug.datastructures import MultiDict from werkzeug.utils import secure_filename from flask import redirect, render_template, flash, url_for, request, \ session from flask.ext.login import current_user from flask.ext.restful import fields, marshal from invenio.ext.restful import UTCISODateTime from invenio.base.helpers import unicodifier from invenio.ext.sqlalchemy import db -from invenio.modules.workflows.config import CFG_OBJECT_VERSION, CFG_WORKFLOW_STATUS -from invenio.modules.workflows.models import BibWorkflowObject, Workflow -from invenio.modules.workflows.engine import BibWorkflowEngine +from invenio.modules.workflows.models import BibWorkflowObject, Workflow, ObjectVersion +from invenio.modules.workflows.engine import BibWorkflowEngine, WorkflowStatus from invenio.modules.workflows.api import continue_oid from . import forms from .form import CFG_FIELD_FLAGS, DataExporter from .signals import file_uploaded from .storage import Storage, DepositionStorage # # Exceptions # class DepositionError(Exception): """ Base class for deposition errors """ pass class InvalidDepositionType(DepositionError): """ Raised when a deposition type cannot be found """ pass class DepositionDoesNotExists(DepositionError): """ Raised when a deposition does not exists """ pass class DraftDoesNotExists(DepositionError): """ Raised when a draft does not exists """ pass class FormDoesNotExists(DepositionError): """ Raised when a draft does not exists """ pass class FileDoesNotExists(DepositionError): """ Raised when a draft does not exists """ pass class DepositionNotDeletable(DepositionError): """ Raised when a deposition cannot be deleted """ pass class FilenameAlreadyExists(DepositionError): """ Raised when an identical filename is already present in a deposition""" pass class ForbiddenAction(DepositionError): """ Raised when an action on a deposition, draft or file is not authorized """ pass class InvalidApiAction(DepositionError): """ Raised when an invalid API action is requested """ pass # # Helpers # class FactoryMixin(object): """ Mix-in class to help create objects from persisted object state. """ @classmethod def factory(cls, state, *args, **kwargs): obj = cls(*args, **kwargs) obj.__setstate__(state) return obj # # Primary classes # class DepositionType(object): """ A base class for the deposition types to ensure certain properties are defined on each type. A deposition type is just a BibWorkflow with a couple of extra methods. To customize rendering behavior of the workflow for a given deposition type you can override the render_error(), render_step() and render_completed() methods. """ workflow = [] """ Workflow definition """ name = "" """ Display name for this deposition type """ name_plural = "" """ Plural version of display name for this deposition type """ enabled = False """ Determines if type is enabled - TODO: REMOVE""" default = False """ Determines if type is the default - warnings are issed if conflicts exsists TODO: remove """ deletable = False """ Determine if a deposition is deletable after submission. """ editable = False """ Determine if a deposition is editable after submission. """ stopable = False """ Determine if a deposition workflow can be stopped (i.e. discard changes). """ group = None """ Name of group to include this type in. """ api = False """ Determines if API is enabled for this type (requires workflow to be compatible with the API). """ draft_definitions = {'_default': None} """ Dictionary of all drafts for this deposition type """ marshal_file_fields = dict( checksum=fields.String, filename=fields.String(attribute='name'), id=fields.String(attribute='uuid'), filesize=fields.String(attribute='size'), ) """ REST API structure of a file """ marshal_draft_fields = dict( metadata=fields.Raw(attribute='values'), completed=fields.Boolean, id=fields.String, ) """ REST API structure of a draft """ marshal_deposition_fields = dict( id=fields.Integer, title=fields.String, created=UTCISODateTime, modified=UTCISODateTime, owner=fields.Integer(attribute='user_id'), state=fields.String, submitted=fields.Boolean, files=fields.Nested(marshal_file_fields), drafts=fields.Nested(marshal_draft_fields, attribute='drafts_list'), ) """ REST API structure of a deposition """ @classmethod def default_draft_id(cls, deposition): return '_default' @classmethod def render_error(cls, dummy_deposition): """ Render a page when deposition had an workflow error. Method can be overwritten by subclasses to provide custom user interface. """ flash('%(name)s deposition has returned error.' % {'name': cls.name}, 'error') return redirect(url_for('.index')) @classmethod def render_step(self, deposition): """ Render a page for a given deposition step. Method can be overwritten by subclasses to provide custom user interface. """ ctx = deposition.get_render_context() if ctx: return render_template(**ctx) else: return render_template('deposit/error.html', **dict( depostion=deposition, deposition_type=( None if deposition.type.is_default() else deposition.type.get_identifier() ), uuid=deposition.id, my_depositions=Deposition.get_depositions( current_user, type=deposition.type ), )) @classmethod def render_completed(cls, dummy_deposition): """ Render page when deposition was successfully completed (i.e workflow just finished successfully). Method can be overwritten by subclasses to provide custom user interface. """ flash('%(name)s was successfully finished.' % {'name': cls.name}, 'success') return redirect(url_for('.index')) @classmethod def render_final(cls, deposition): """ Render page when deposition was *already* successfully completed (i.e a finished workflow is being executed a second time). This allows you render e.g. a preview of the record. The distinction between render_completed and render_final is primarily useful for the REST API (see api_final and api_completed) Method can be overwritten by subclasses to provide custom user interface. """ return cls.render_completed(deposition) @classmethod def api_completed(cls, deposition): """ Workflow just finished processing so return an 202 Accepted, since usually further background processing may happen. """ return deposition.marshal(), 202 @classmethod def api_final(cls, deposition): """ Workflow already finished, and the user tries to re-execute the workflow, so send a 400 Bad Request back. """ return dict( message="Deposition workflow already completed", status=400, ), 400 @classmethod def api_step(cls, deposition): """ Workflow was halted during processing. The workflow task that halted processing is expected to provide a response to send back to the client. The default response code is 500 Internal Server Error. A workflow task is expected to use Deposition.set_render_context() with a dictionary which is returned to the client. Set the key 'status', to change the status code, e.g.:: d.set_render_context(dict(status=400, message="Bad request")) If no response is provided by the workflow task, it is regarded as an internal server error. """ ctx = deposition.get_render_context() if ctx: return ctx.get('response', {}), ctx.get('status', 500) return cls.api_error(deposition) @classmethod def api_error(cls, deposition): return dict(message='Internal Server Error', status=500), 500 @classmethod def api_action(cls, deposition, action_id): if action_id == 'run': return deposition.run_workflow(headless=True) elif action_id == 'reinitialize': deposition.reinitialize_workflow() return deposition.run_workflow(headless=True) elif action_id == 'stop': deposition.stop_workflow() return deposition.run_workflow(headless=True) raise InvalidApiAction(action_id) @classmethod def api_metadata_schema(cls, draft_id): """ Get the input validation schema for this draft_id Allows you to override API defaults. """ from wtforms.fields.core import FieldList, FormField if draft_id in cls.draft_definitions: schema = dict() formclass = cls.draft_definitions[draft_id] for fname, fclass in formclass()._fields.items(): if isinstance(fclass, FieldList): schema[fname] = dict(type='list') elif isinstance(fclass, FormField): schema[fname] = dict(type='dict') else: schema[fname] = dict(type='any') return dict(type='dict', schema=schema) return None @classmethod def marshal_deposition(cls, obj): """ Generate a JSON representation for REST API of a Deposition """ return marshal(obj, cls.marshal_deposition_fields) @classmethod def marshal_draft(cls, obj): """ Generate a JSON representation for REST API of a DepositionDraft """ return marshal(obj, cls.marshal_draft_fields) @classmethod def marshal_file(cls, obj): """ Generate a JSON representation for REST API of a DepositionFile """ return marshal(obj, cls.marshal_file_fields) @classmethod def authorize(cls, deposition, action): if action == 'create': return True # Any authenticated user elif action == 'delete': if deposition.has_sip(): return deposition.type.deletable return True elif action == 'reinitialize': return deposition.type.editable elif action == 'stop': return deposition.type.stopable elif action in ['add_file', 'remove_file', 'sort_files']: # Don't allow to add/remove/sort files after first submission return not deposition.has_sip() elif action in ['add_draft', ]: # Allow adding drafts when inprogress (independent of SIP exists # or not). return deposition.state == 'inprogress' else: return not deposition.has_sip() @classmethod def authorize_draft(cls, deposition, draft, action): if action == 'update': # If deposition allows adding a draft, then allow editing the # draft. return cls.authorize(deposition, 'add_draft') return cls.authorize(deposition, 'add_draft') @classmethod def authorize_file(cls, deposition, deposition_file, action): return cls.authorize(deposition, 'add_file') @classmethod def get_identifier(cls): """ Get type identifier (identical to workflow name) """ return cls.__name__ @classmethod def is_enabled(cls): """ Check if workflow is enabled """ # Wrapping in a method to eventually allow enabling/disabling # via configuration. return cls.enabled @classmethod def is_default(cls): """ Check if workflow is the default """ # Wrapping in a method to eventually allow configuration # via configuration. return cls.default @classmethod def run_workflow(cls, deposition): """ Run workflow for the given BibWorkflowObject. Usually not invoked directly, but instead indirectly through Deposition.run_workflow(). """ return continue_oid( deposition.id, start_point="restart_task", ) @classmethod def reinitialize_workflow(cls, deposition): # Only reinitialize if really needed (i.e. you can only # reinitialize a fully completed workflow). wo = deposition.workflow_object - if wo.version == CFG_OBJECT_VERSION.FINAL and \ - wo.workflow.status == CFG_WORKFLOW_STATUS.COMPLETED: + if wo.version == ObjectVersion.FINAL and \ + wo.workflow.status == WorkflowStatus.COMPLETED: - wo.version = CFG_OBJECT_VERSION.RUNNING - wo.workflow.status = CFG_WORKFLOW_STATUS.RUNNING + wo.version = ObjectVersion.RUNNING + wo.workflow.status = WorkflowStatus.RUNNING # Clear deposition drafts deposition.drafts = {} @classmethod def stop_workflow(cls, deposition): # Only stop workflow if really needed wo = deposition.workflow_object - if wo.version != CFG_OBJECT_VERSION.FINAL and \ - wo.workflow.status != CFG_WORKFLOW_STATUS.COMPLETED: + if wo.version != ObjectVersion.FINAL and \ + wo.workflow.status != WorkflowStatus.COMPLETED: # Only workflows which has been fully completed once before # can be stopped if deposition.has_sip(): - wo.version = CFG_OBJECT_VERSION.FINAL - wo.workflow.status = CFG_WORKFLOW_STATUS.COMPLETED + wo.version = ObjectVersion.FINAL + wo.workflow.status = WorkflowStatus.COMPLETED # Clear all drafts deposition.drafts = {} # Set title - FIXME: find better way to set title sip = deposition.get_latest_sip(sealed=True) title = sip.metadata.get('title', 'Untitled') deposition.title = title @classmethod def all(cls): """ Get a dictionary of deposition types """ from .registry import deposit_types return deposit_types.mapping() @classmethod def get(cls, identifier): try: return cls.all()[identifier] except KeyError: raise InvalidDepositionType(identifier) @classmethod def keys(cls): """ Get a list of deposition type names """ return cls.all().keys() @classmethod def values(cls): """ Get a list of deposition type names """ return cls.all().values() @classmethod def get_default(cls): """ Get a list of deposition type names """ from .registry import deposit_default_type return deposit_default_type.get() def __unicode__(self): """ Return a name for this class """ return self.get_identifier() class DepositionFile(FactoryMixin): """ Represents an uploaded file Creating a normal deposition file:: uploaded_file = request.files['file'] filename = secure_filename(uploaded_file.filename) backend = DepositionStorage(deposition_id) d = DepositionFile(backend=backend) d.save(uploaded_file, filename) Creating a chunked deposition file:: uploaded_file = request.files['file'] filename = secure_filename(uploaded_file.filename) chunk = request.files['chunk'] chunks = request.files['chunks'] backend = ChunkedDepositionStorage(deposition_id) d = DepositionFile(id=file_id, backend=backend) d.save(uploaded_file, filename, chunk, chunks) if chunk == chunks: d.save(finish=True, filename=filename) Reading a file:: d = DepositionFile.from_json(data) if d.is_local(): send_file(d.get_syspath()) else: redirect(d.get_url()) d.delete() Deleting a file:: d = DepositionFile.from_json(data) d.delete() """ def __init__(self, uuid=None, backend=None): self.uuid = uuid or str(uuid4()) self._backend = backend self.name = '' def __getstate__(self): # TODO: Add content_type attributes return dict( id=self.uuid, path=self.path, name=self.name, size=self.size, checksum=self.checksum, #bibdoc=self.bibdoc ) def __setstate__(self, state): self.uuid = state['id'] self._path = state['path'] self.name = state['name'] self.size = state['size'] self.checksum = state['checksum'] def __repr__(self): data = self.__getstate__() del data['path'] return json.dumps(data) @property def backend(self): if not self._backend: self._backend = Storage(None) return self._backend @property def path(self): if self._path is None: raise Exception("No path set") return self._path def save(self, incoming_file, filename=None, *args, **kwargs): self.name = secure_filename(filename or incoming_file.filename) (self._path, self.size, self.checksum, result) = self.backend.save( incoming_file, filename, *args, **kwargs ) return result def delete(self): """ Delete the file on storage """ return self.backend.delete(self.path) def is_local(self): """ Determine if file is a local file """ return self.backend.is_local(self.path) def get_url(self): """ Get a URL for the file """ return self.backend.get_url(self.path) def get_syspath(self): """ Get a local system path to the file """ return self.backend.get_syspath(self.path) class DepositionDraftCacheManager(object): """ Draft cache manager takes care of storing draft values in the cache prior to a workflow being run. The data can be loaded by the prefill_draft() workflow task. """ def __init__(self, user_id): self.user_id = user_id self.data = {} @classmethod def from_request(cls): """ Create a new draft cache from the current request. """ obj = cls(current_user.get_id()) # First check if we can get it via a json data = request.get_json(silent=True) if not data: # If, not simply merge all both query parameters and request body # parameters. data = request.values.to_dict() obj.data = data return obj @classmethod def get(cls): obj = cls(current_user.get_id()) obj.load() return obj def save(self): """ Save data to session """ if self.has_data(): session['deposit_prefill'] = self.data session.modified = True else: self.delete() def load(self): """ Load data from session """ self.data = session.get('deposit_prefill', {}) def delete(self): """ Delete data in session """ if 'deposit_prefill' in session: del session['deposit_prefill'] session.modified = True def has_data(self): """ Determine if the cache has data. """ return bool(self.data) def fill_draft(self, deposition, draft_id, clear=True): """ Fill a draft with cached draft values """ draft = deposition.get_or_create_draft(draft_id) draft.process(self.data) if clear: self.data = {} self.delete() return draft class DepositionDraft(FactoryMixin): """ Represents the state of a form """ def __init__(self, draft_id, form_class=None, deposition_ref=None): self.id = draft_id self.completed = False self.form_class = form_class self.values = {} self.flags = {} self._form = None # Back reference to the depositions self._deposition_ref = deposition_ref self.validate = False def __getstate__(self): return dict( completed=self.completed, values=self.values, flags=self.flags, validate=self.validate, ) def __setstate__(self, state): self.completed = state['completed'] self.form_class = None if self._deposition_ref: self.form_class = self._deposition_ref.type.draft_definitions.get( self.id ) self.values = state['values'] self.flags = state['flags'] self.validate = state.get('validate', True) def is_completed(self): return self.completed def has_form(self): return self.form_class is not None def authorize(self, action): if not self._deposition_ref: return True # Not connected to deposition so authorize anything. return self._deposition_ref.type.authorize_draft( self._deposition_ref, self, action ) def complete(self): """ Set state of draft to completed. """ self.completed = True def update(self, form): """ Update draft values and flags with data from form. """ data = dict((key, value) for key, value in form.data.items() if value is not None) self.values = data self.flags = form.get_flags() def process(self, data, complete_form=False): """ Process, validate and store incoming form data and return response. """ if not self.authorize('update'): raise ForbiddenAction('update', self) if not self.has_form(): raise FormDoesNotExists(self.id) # The form is initialized with form and draft data. The original # draft_data is accessible in Field.object_data, Field.raw_data is the # new form data and Field.data is the processed form data or the # original draft data. # # Behind the scences, Form.process() is called, which in turns call # Field.process_data(), Field.process_formdata() and any filters # defined. # # Field.object_data contains the value of process_data(), while # Field.data contains the value of process_formdata() and any filters # applied. form = self.get_form(formdata=data) # Run form validation which will call Field.pre_valiate(), # Field.validators, Form.validate_<field>() and Field.post_validate(). # Afterwards Field.data has been validated and any errors will be # present in Field.errors. validated = form.validate() # Call Form.run_processors() which in turn will call # Field.run_processors() that allow fields to set flags (hide/show) # and values of other fields after the entire formdata has been # processed and validated. validated_flags, validated_data, validated_msgs = ( form.get_flags(), form.data, form.messages ) form.post_process(formfields=[] if complete_form else data.keys()) post_processed_flags, post_processed_data, post_processed_msgs = ( form.get_flags(), form.data, form.messages ) # Save form values self.update(form) # Build result dictionary process_field_names = None if complete_form else data.keys() # Determine if some fields where changed during post-processing. changed_values = dict( (name, value) for name, value in post_processed_data.items() if validated_data[name] != value ) # Determine changed flags changed_flags = dict( (name, flags) for name, flags in post_processed_flags.items() if validated_flags[name] != flags ) # Determine changed messages changed_msgs = dict( (name, messages) for name, messages in post_processed_msgs.items() if validated_msgs[name] != messages or process_field_names is None or name in process_field_names ) result = {} if changed_msgs: result['messages'] = changed_msgs if changed_values: result['values'] = changed_values if changed_flags: for flag in CFG_FIELD_FLAGS: fields = [ (name, flag in field_flags) for name, field_flags in changed_flags.items() ] result[flag + '_on'] = map( lambda x: x[0], filter(lambda x: x[1], fields) ) result[flag + '_off'] = map( lambda x: x[0], filter(lambda x: not x[1], fields) ) return form, validated, result def get_form(self, formdata=None, load_draft=True, validate_draft=False): """ Create form instance with draft data and form data if provided. :param formdata: Incoming form data. :param files: Files to ingest into form :param load_draft: True to initialize form with draft data. :param validate_draft: Set to true to validate draft data, when no form data is provided. """ if not self.has_form(): raise FormDoesNotExists(self.id) # If a field is not present in formdata, Form.process() will assume it # is blank instead of using the draft_data value. Most of the time we # are only submitting a single field in JSON via AJAX requests. We # therefore reset non-submitted fields to the draft_data value with # form.reset_field_data(). # WTForms deal with unicode - we deal with UTF8 so convert all draft_data = unicodifier(self.values) if load_draft else {} formdata = MultiDict(formdata or {}) form = self.form_class( formdata=formdata, **draft_data ) if formdata: form.reset_field_data(exclude=formdata.keys()) # Set field flags if load_draft and self.flags: form.set_flags(self.flags) # Ingest files in form if self._deposition_ref: form.files = self._deposition_ref.files else: form.files = [] if validate_draft and draft_data and formdata is None: form.validate() return form @classmethod def merge_data(cls, drafts): """ Merge data of multiple drafts Duplicate keys will be overwritten without warning. """ data = {} # Don't include *) disabled fields, and *) empty optional fields func = lambda f: not f.flags.disabled and (f.flags.required or f.data) for d in drafts: if d.has_form(): visitor = DataExporter( filter_func=func ) visitor.visit(d.get_form()) data.update(visitor.data) else: data.update(d.values) return data class Deposition(object): """ Wraps a BibWorkflowObject Basically an interface to work with BibWorkflowObject data attribute in an easy manner. """ def __init__(self, workflow_object, type=None, user_id=None): self.workflow_object = workflow_object if not workflow_object: self.files = [] self.drafts = {} self.type = self.get_type(type) self.title = '' self.sips = [] self.engine = BibWorkflowEngine( name=self.type.get_identifier(), id_user=user_id, module_name="webdeposit" ) self.workflow_object = BibWorkflowObject( id_workflow=self.engine.uuid, id_user=user_id, - version=CFG_OBJECT_VERSION.RUNNING, + version=ObjectVersion.RUNNING, ) self.workflow_object.set_data({}) else: self.__setstate__(workflow_object.get_data()) self.engine = None # # Properties proxies to BibWorkflowObject # @property def id(self): return self.workflow_object.id @property def user_id(self): return self.workflow_object.id_user @property def created(self): return self.workflow_object.created @property def modified(self): return self.workflow_object.modified @property def drafts_list(self): # Needed for easy marshaling by API return self.drafts.values() # # Proxy methods # def authorize(self, action): """ Determine if certain action is authorized Delegated to deposition type to allow overwriting default behavior. """ return self.type.authorize(self, action) # # Serialization related methods # def marshal(self): """ API representation of an object. Delegated to the DepositionType, to allow overwriting default behaviour. """ return self.type.marshal_deposition(self) def __getstate__(self): """ Serialize deposition state for storing in the BibWorkflowObject """ # The bibworkflow object id and owner is implicit, as the Deposition # object only wraps the data attribute of a BibWorkflowObject. # FIXME: Find better solution for setting the title. for d in self.drafts.values(): if 'title' in d.values: self.title = d.values['title'] break return dict( type=self.type.get_identifier(), title=self.title, files=[f.__getstate__() for f in self.files], drafts=dict( [(d_id, d.__getstate__()) for d_id, d in self.drafts.items()] ), sips=[f.__getstate__() for f in self.sips], ) def __setstate__(self, state): """ Deserialize deposition from state stored in BibWorkflowObject """ self.type = DepositionType.get(state['type']) self.title = state['title'] self.files = [ DepositionFile.factory( f_state, uuid=f_state['id'], backend=DepositionStorage(self.id), ) for f_state in state['files'] ] self.drafts = dict( [(d_id, DepositionDraft.factory(d_state, d_id, deposition_ref=self)) for d_id, d_state in state['drafts'].items()] ) self.sips = [ SubmissionInformationPackage.factory(s_state, uuid=s_state['id']) for s_state in state.get('sips', []) ] # # Persistence related methods # def update(self): """ Update workflow object with latest data. """ data = self.__getstate__() # BibWorkflow calls get_data() before executing any workflow task, and # and calls set_data() after. Hence, unless we update the data # attribute it will be overwritten. try: self.workflow_object.data = data except AttributeError: pass self.workflow_object.set_data(data) def reload(self): """ Get latest data from workflow object """ self.__setstate__(self.workflow_object.get_data()) def save(self): """ Save the state of the deposition. Uses the __getstate__ method to make a JSON serializable representation which, sets this as data on the workflow object and saves it. """ self.update() if self.engine: - self.engine.save(status=CFG_WORKFLOW_STATUS.RUNNING) + self.engine.save(status=WorkflowStatus.RUNNING) self.workflow_object.save( - version=self.workflow_object.version or CFG_OBJECT_VERSION.RUNNING + version=self.workflow_object.version or ObjectVersion.RUNNING ) def delete(self): """ Delete the current deposition """ if not self.authorize('delete'): raise DepositionNotDeletable(self) for f in self.files: f.delete() if self.workflow_object.id_workflow != '': if self.workflow_object.id_workflow: Workflow.delete(uuid=self.workflow_object.id_workflow) BibWorkflowObject.query.filter_by( id_workflow=self.workflow_object.id_workflow ).delete() else: db.session.remove(self.workflow_object) db.session.commit() # # Workflow execution # def run_workflow(self, headless=False): """ Execute the underlying workflow If you made modifications to the deposition you must save if before running the workflow, using the save() method. """ current_status = self.workflow_object.workflow.status - if current_status == CFG_WORKFLOW_STATUS.COMPLETED: + if current_status == WorkflowStatus.COMPLETED: return self.type.api_final(self) if headless \ else self.type.render_final(self) self.update() status = self.type.run_workflow(self).status self.reload() - if status == CFG_WORKFLOW_STATUS.ERROR: + if status == WorkflowStatus.ERROR: return self.type.api_error(self) if headless else \ self.type.render_error(self) - elif status != CFG_WORKFLOW_STATUS.COMPLETED: + elif status != WorkflowStatus.COMPLETED: return self.type.api_step(self) if headless else \ self.type.render_step(self) - elif status == CFG_WORKFLOW_STATUS.COMPLETED: + elif status == WorkflowStatus.COMPLETED: return self.type.api_completed(self) if headless else \ self.type.render_completed(self) def reinitialize_workflow(self): """ Reinitialize a workflow object (i.e. prepare it for editing) """ if self.state != 'done': raise DepositionError("Action only allowed for depositions in " "state 'done'.") if not self.authorize('reinitialize'): raise ForbiddenAction('reinitialize', self) self.type.reinitialize_workflow(self) def stop_workflow(self): """ Stop a running workflow object (e.g. discard changes while editing). """ if self.state != 'inprogress' or not self.submitted: raise DepositionError("Action only allowed for depositions in " "state 'inprogress'.") if not self.authorize('stop'): raise ForbiddenAction('stop', self) self.type.stop_workflow(self) def set_render_context(self, ctx): """ Set rendering context - used in workflow tasks to set what is to be rendered (either by API or UI) """ self.workflow_object.deposition_context = ctx def get_render_context(self): """ Get rendering context - used by DepositionType.render_step/api_step """ return getattr(self.workflow_object, 'deposition_context', {}) @property def state(self): """ Return simplified workflow state - inprogress, done or error """ try: status = self.workflow_object.workflow.status - if status == CFG_WORKFLOW_STATUS.ERROR: + if status == WorkflowStatus.ERROR: return "error" - elif status == CFG_WORKFLOW_STATUS.COMPLETED: + elif status == WorkflowStatus.COMPLETED: return "done" except AttributeError: pass return "inprogress" # # Draft related methods # def get_draft(self, draft_id): """ Get draft """ if draft_id not in self.drafts: raise DraftDoesNotExists(draft_id) return self.drafts[draft_id] def get_or_create_draft(self, draft_id): """ Get or create a draft for given draft_id """ if draft_id not in self.drafts: if draft_id not in self.type.draft_definitions: raise DraftDoesNotExists(draft_id) if not self.authorize('add_draft'): raise ForbiddenAction('add_draft', self) self.drafts[draft_id] = DepositionDraft( draft_id, form_class=self.type.draft_definitions[draft_id], deposition_ref=self, ) return self.drafts[draft_id] def get_default_draft_id(self): """ Get the default draft id for this deposition. """ return self.type.default_draft_id(self) # # Submission information package related methods # def get_latest_sip(self, sealed=None): """ Get the latest submission information package :param sealed: Set to true to only returned latest sealed SIP. Set to False to only return latest unsealed SIP. """ if len(self.sips) > 0: for sip in reversed(self.sips): if sealed is None: return sip elif sealed and sip.is_sealed(): return sip elif not sealed and not sip.is_sealed(): return sip return None def create_sip(self): """ Create a new submission information package (SIP) with metadata from the drafts. """ metadata = DepositionDraft.merge_data(self.drafts.values()) metadata['files'] = map( lambda x: dict(path=x.path, name=os.path.splitext(x.name)[0]), self.files ) sip = SubmissionInformationPackage(metadata=metadata) self.sips.append(sip) return sip def has_sip(self, sealed=True): """ Determine if deposition has a sealed submission information package. """ for sip in self.sips: if (sip.is_sealed() and sealed) or \ (not sealed and not sip.is_sealed()): return True return False @property def submitted(self): return self.has_sip() # # File related methods # def get_file(self, file_id): for f in self.files: if f.uuid == file_id: return f return None def add_file(self, deposition_file): if not self.authorize('add_file'): raise ForbiddenAction('add_file', self) for f in self.files: if f.name == deposition_file.name: raise FilenameAlreadyExists(deposition_file.name) self.files.append(deposition_file) file_uploaded.send( self.type.get_identifier(), deposition=self, deposition_file=deposition_file, ) def remove_file(self, file_id): if not self.authorize('remove_file'): raise ForbiddenAction('remove_file', self) idx = None for i, f in enumerate(self.files): if f.uuid == file_id: idx = i if idx is not None: return self.files.pop(idx) return None def sort_files(self, file_id_list): """ Order the files according the list of ids provided to this function. """ if not self.authorize('sort_files'): raise ForbiddenAction('sort_files', self) search_dict = dict( [(f, i) for i, f in enumerate(file_id_list)] ) def _sort_files_cmp(f_x, f_y): i_x = search_dict.get(f_x.uuid, None) i_y = search_dict.get(f_y.uuid, None) if i_x == i_y: return 0 elif i_x is None or i_x > i_y: return 1 elif i_y is None or i_x < i_y: return -1 self.files = sorted(self.files, _sort_files_cmp) # # Class methods # @classmethod def get_type(self, type_or_id): if type_or_id and isinstance(type_or_id, type) and \ issubclass(type_or_id, DepositionType): return type_or_id else: return DepositionType.get(type_or_id) if type_or_id else \ DepositionType.get_default() @classmethod def create(cls, user, type=None): """ Create a new deposition object. To persist the deposition, you must call save() on the created object. If no type is defined, the default deposition type will be assigned. @param user: The owner of the deposition @param type: Deposition type identifier. """ t = cls.get_type(type) if not t.authorize(None, 'create'): raise ForbiddenAction('create') obj = cls(None, type=type, user_id=user.get_id()) return obj @classmethod def get(cls, object_id, user=None, type=None): """ Get the deposition with specified object id. @param object_id: The BibWorkflowObject id. @param user: Owner of the BibWorkflowObject @param type: Deposition type identifier. """ if type: type = DepositionType.get(type) try: workflow_object = BibWorkflowObject.query.filter_by( id=object_id ).one() except NoResultFound: raise DepositionDoesNotExists(object_id) if user and workflow_object.id_user != user.get_id(): raise DepositionDoesNotExists(object_id) obj = cls(workflow_object) if type and obj.type != type: raise DepositionDoesNotExists(object_id, type) return obj @classmethod def get_depositions(cls, user=None, type=None): params = [ Workflow.module_name == 'webdeposit', ] if user: params.append(BibWorkflowObject.id_user == user.get_id()) if type: params.append(Workflow.name == type.get_identifier()) objects = BibWorkflowObject.query.join("workflow").options( db.contains_eager('workflow')).filter(*params).order_by( BibWorkflowObject.modified.desc()).all() def _create_obj(o): obj = cls(o) if type is None or obj.type == type: return obj return None return filter(lambda x: x is not None, map(_create_obj, objects)) class SubmissionInformationPackage(FactoryMixin): """ Submission information package (SIP) :param uuid: Unique identifier for this SIP :param metadata: Metadata in JSON for this submission information package :param package: Full generated metadata for this package (i.e. normally MARC for records, but could anything). :param timestamp: UTC timestamp in ISO8601 format of when package was sealed. :param agents: List of agents for this package (e.g. creator, ...) :param task_ids: List of task ids submitted to ingest this package (may be appended to after SIP has been sealed). """ def __init__(self, uuid=None, metadata={}): self.uuid = uuid or str(uuid4()) self.metadata = metadata self.package = "" self.timestamp = None self.agents = [] self.task_ids = [] def __getstate__(self): return dict( id=self.uuid, metadata=self.metadata, package=self.package, timestamp=self.timestamp, task_ids=self.task_ids, agents=[a.__getstate__() for a in self.agents], ) def __setstate__(self, state): self.uuid = state['id'] self._metadata = state.get('metadata', {}) self.package = state.get('package', None) self.timestamp = state.get('timestamp', None) self.agents = [Agent.factory(a_state) for a_state in state.get('agents', [])] self.task_ids = state.get('task_ids', []) def seal(self): self.timestamp = datetime.now(tzutc()).isoformat() def is_sealed(self): return self.timestamp is not None @property def metadata(self): return self._metadata @metadata.setter def metadata(self, value): import datetime import json class DateTimeEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date): encoded_object = obj.isoformat() else: encoded_object = json.JSONEncoder.default(self, obj) return encoded_object data = json.dumps(value, cls=DateTimeEncoder) self._metadata = json.loads(data) class Agent(FactoryMixin): def __init__(self, role=None, from_request_context=False): self.role = role self.user_id = None self.ip_address = None self.email_address = None if from_request_context: self.from_request_context() def __getstate__(self): return dict( role=self.role, user_id=self.user_id, ip_address=self.ip_address, email_address=self.email_address, ) def __setstate__(self, state): self.role = state['role'] self.user_id = state['user_id'] self.ip_address = state['ip_address'] self.email_address = state['email_address'] def from_request_context(self): from flask import request from invenio.ext.login import current_user self.ip_address = request.remote_addr self.user_id = current_user.get_id() self.email_address = current_user.info.get('email', '') diff --git a/invenio/modules/workflows/api.py b/invenio/modules/workflows/api.py index a5793e2cb..21472543e 100644 --- a/invenio/modules/workflows/api.py +++ b/invenio/modules/workflows/api.py @@ -1,299 +1,298 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ BibWorkflow API - functions to run workflows """ from werkzeug.utils import (import_string, cached_property) from invenio.base.globals import cfg from invenio.base.config import CFG_BIBWORKFLOW_WORKER from .utils import BibWorkflowObjectIdContainer from .models import BibWorkflowObject from .errors import WorkflowWorkerError class WorkerBackend(object): @cached_property def worker(self): try: return import_string('invenio.modules.workflows.workers.%s:%s' % ( cfg['CFG_BIBWORKFLOW_WORKER'], cfg['CFG_BIBWORKFLOW_WORKER'])) except: from invenio.ext.logging import register_exception ## Let's report about broken plugins register_exception(alert_admin=True) def __call__(self, *args, **kwargs): if not self.worker: raise WorkflowWorkerError('No worker configured') return self.worker(*args, **kwargs) WORKER = WorkerBackend() def start(workflow_name, data, **kwargs): """ Starts a workflow by given name for specified data *immediately* in the current process. The name of the workflow to start is considered unique and it is equal to the name of a file containing the workflow definition. The data passed should be a list of object(s) to run through the workflow. For example: a list of dict, JSON string, BibWorkflowObjects etc. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. The workflow engine object generated is returned upon completion. @param workflow_name: the workflow name to run. Ex: "my_workflow" @type workflow_name: str @param data: the workflow name to run. Ex: "my_workflow" @type data: list of objects/dicts @return: BibWorkflowEngine that ran the workflow. """ from .worker_engine import run_worker return run_worker(workflow_name, data, **kwargs) def start_delayed(workflow_name, data, **kwargs): """ Starts a *delayed* workflow by using one of the defined workers available. For example, enqueueing the execution of the workflow in a task queue such as Celery (http://celeryproject.org). Otherwise, see documentation of start(). @param workflow_name: the workflow name to run. Ex: "my_workflow" @type workflow_name: str @param data: the workflow name to run. Ex: "my_workflow" @type data: list of objects/dicts @return: BibWorkflowEngine that ran the workflow. """ if not CFG_BIBWORKFLOW_WORKER: raise WorkflowWorkerError('No worker configured') #The goal of this part is to avoid a SQLalchemy decoherence in case #some one try to send a Bibworkflow object. To avoid to send the #complete object and get SQLAlchemy error of mapping, we save the id #into our Id container, In the celery process the object is reloaded #from the database ! if isinstance(data, list): for i in range(0, len(data)): if isinstance(data[i], BibWorkflowObject): data[i] = BibWorkflowObjectIdContainer(data[i]).to_dict() else: if isinstance(data, BibWorkflowObject): data = BibWorkflowObjectIdContainer(data).to_dict() return WORKER().run_worker(workflow_name, data, **kwargs) def start_by_wid(wid, **kwargs): """ Will re-start given workflow, by workflow uuid (wid), from the beginning with the original data given. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. @param wid: the workflow uuid. Ex: "550e8400-e29b-41d4-a716-446655440000" @type wid: string @return: BibWorkflowEngine that ran the workflow. """ from .worker_engine import restart_worker return restart_worker(wid, **kwargs) def start_by_wid_delayed(wid, **kwargs): """ Will re-start given workflow, by workflow uuid (wid), from the beginning with the original data given. Starts the workflow *delayed* by using one of the defined workers available. For example, enqueueing the execution of the workflow in a task queue such as Celery (http://celeryproject.org). Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. @param wid: the workflow uuid. Ex: "550e8400-e29b-41d4-a716-446655440000" @type wid: string @return: BibWorkflowEngine that ran the workflow. """ return WORKER().restart_worker(wid, **kwargs) def start_by_oids(workflow_name, oids, **kwargs): """ Will start given workflow, by name, using the given list of BibWorkflowObject ids (oids) from beginning. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. @param workflow_name: the workflow name to run. Ex: "my_workflow" @type workflow_name: str @param oids: list of BibWorkflowObject id's to run. @type oids: list of strings/integers @return: BibWorkflowEngine that ran the workflow. """ from .models import BibWorkflowObject objects = BibWorkflowObject.query.filter( BibWorkflowObject.id.in_(list(oids)) ).all() return start(workflow_name, objects, **kwargs) def start_by_oids_delayed(workflow_name, oids, **kwargs): """ Will start given workflow, by name, using the given list of BibWorkflowObject ids (oids) from beginning. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. Starts the workflow *delayed* by using one of the defined workers available. For example, enqueueing the execution of the workflow in a task queue such as Celery (http://celeryproject.org). @param workflow_name: the workflow name to run. Ex: "my_workflow" @type workflow_name: str @param oids: list of BibWorkflowObject id's to run. @type oids: list of strings/integers @return: BibWorkflowEngine that ran the workflow. """ from .models import BibWorkflowObject objects = BibWorkflowObject.query.filter( BibWorkflowObject.id.in_(list(oids)) ).all() return start_delayed(workflow_name, objects, **kwargs) def continue_oid(oid, start_point="continue_next", **kwargs): """ Continue workflow asociated with object given by object id (oid). It can start from previous, current or next task. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. Starts the workflow *delayed* by using one of the defined workers available. For example, enqueueing the execution of the workflow in a task queue such as Celery (http://celeryproject.org). @param oid: id of BibWorkflowObject to run. @type oid: string @param start_point: where should the workflow start from? One of: * restart_prev: will restart from the previous task * continue_next: will continue to the next task * restart_task: will restart the current task @type start_point: string @return: BibWorkflowEngine that ran the workflow """ from .worker_engine import continue_worker return continue_worker(oid, start_point, **kwargs) def continue_oid_delayed(oid, start_point="continue_next", **kwargs): """ Continue workflow associated with object given by object id (oid). It can start from previous, current or next task. Special custom keyword arguments can be given to the workflow engine in order to pass certain variables to the tasks in the workflow execution, such as a taskid from BibSched, the current user etc. Starts the workflow *delayed* by using one of the defined workers available. For example, enqueueing the execution of the workflow in a task queue such as Celery (http://celeryproject.org). @param oid: id of BibWorkflowObject to run. @type oid: string @param start_point: where should the workflow start from? One of: * restart_prev: will restart from the previous task * continue_next: will continue to the next task * restart_task: will restart the current task @type start_point: string @return: BibWorkflowEngine that ran the workflow """ return WORKER().continue_worker(oid, start_point, **kwargs) def resume_objects_in_workflow(id_workflow, start_point="continue_next", **kwargs): """ Resume workflow for any halted or failed objects from given workflow. This is a generator function and will yield every workflow created per object which needs to be resumed. To identify the original workflow containing the halted objects, the ID (or UUID) of the workflow is required. The starting point to resume the objects from can optionally be given. By default, the objects resume with their next task in the workflow. @param id_workflow: id of Workflow with objects to resume. @type id_workflow: string @param start_point: where should the workflow start from? One of: * restart_prev: will restart from the previous task * continue_next: will continue to the next task * restart_task: will restart the current task @type start_point: string @yield: BibWorkflowEngine that ran the workflow """ - from .models import BibWorkflowObject - from .config import CFG_OBJECT_VERSION + from .models import BibWorkflowObject, ObjectVersion # Resume workflow if there are objects to resume objects = BibWorkflowObject.query.filter( BibWorkflowObject.id_workflow == id_workflow, - BibWorkflowObject.version == CFG_OBJECT_VERSION.HALTED + BibWorkflowObject.version == ObjectVersion.HALTED ).all() for obj in objects: yield continue_oid(oid=obj.id, start_point=start_point, **kwargs) diff --git a/invenio/modules/workflows/client.py b/invenio/modules/workflows/client.py index d8c246606..7b97d7016 100644 --- a/invenio/modules/workflows/client.py +++ b/invenio/modules/workflows/client.py @@ -1,119 +1,119 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. import traceback from .errors import (WorkflowHalt, WorkflowError) -from .config import (CFG_OBJECT_VERSION, - CFG_WORKFLOW_STATUS) +from .models import ObjectVersion +from .engine import WorkflowStatus def run_workflow(wfe, data, stop_on_halt=False, stop_on_error=False, initial_run=True, **kwargs): """ Main function running the workflow. """ while True: try: if initial_run: initial_run = False wfe.process(data) # We processed the workflow. We're done. break else: wfe._unpickled = True wfe.restart('current', 'current') # We processed the restarted workflow. We're done. break except WorkflowHalt as e: # Processing was halted. Lets save current object and continue. # Save current object progress current_obj = wfe.get_current_object() if current_obj: if e.widget: current_obj.add_widget(e.widget, e.message) - current_obj.save(version=CFG_OBJECT_VERSION.HALTED, + current_obj.save(version=ObjectVersion.HALTED, task_counter=wfe.getCurrTaskId(), id_workflow=wfe.uuid) else: wfe.log.warning("No active object found!") # Save workflow progress - wfe.save(status=CFG_WORKFLOW_STATUS.HALTED) + wfe.save(status=WorkflowStatus.HALTED) wfe.setPosition(wfe.getCurrObjId() + 1, [0, 0]) message = "Workflow '%s' halted at task %s with message: %s" % \ (wfe.name, wfe.get_current_taskname() or "Unknown", e.message) wfe.log.warning(message) if stop_on_halt: break except Exception as e: # We print the stacktrace, save the object and continue # unless instructed otherwise. msg = "Error: %r\n%s" % (e, traceback.format_exc()) wfe.log.error(msg) # Changing counter should be moved to wfe object # together with default exception handling wfe.increase_counter_error() - wfe._objects[wfe.getCurrObjId()].save(CFG_OBJECT_VERSION.HALTED, + wfe._objects[wfe.getCurrObjId()].save(ObjectVersion.HALTED, wfe.getCurrTaskId(), id_workflow=wfe.uuid) - wfe.save(CFG_WORKFLOW_STATUS.ERROR) + wfe.save(WorkflowStatus.ERROR) wfe.setPosition(wfe.getCurrObjId() + 1, [0, 0]) # if stop_on_halt or stop_on_error: if isinstance(e, WorkflowError): raise e else: raise WorkflowError(message=msg, id_workflow=wfe.uuid, id_object=wfe.getCurrObjId()) def continue_execution(wfe, data, restart_point="restart_task", stop_on_halt=False, stop_on_error=False, **kwargs): """ Continue execution of workflow for given object (wfe) from "restart_point". restart_point can be one of: * restart_prev: will restart from the previous task * continue_next: will continue to the next task * restart_task: will restart the current task You can use stop_on_error to raise exception's and stop the processing. Use stop_on_halt to stop processing the workflow if HaltProcessing is raised. """ wfe.log.info("Continue execution from: %s" % (str(restart_point),)) pos = data[0].get_current_task() if restart_point == "restart_prev": pos[-1] -= 1 wfe.setPosition(wfe.db_obj.current_object, pos) elif restart_point == "continue_next": pos[-1] += 1 wfe.setPosition(wfe.db_obj.current_object, pos) else: # restart_task wfe.setPosition(wfe.db_obj.current_object, pos) wfe._objects = data run_workflow(wfe, data, stop_on_halt, stop_on_error, initial_run=False, **kwargs) diff --git a/invenio/modules/workflows/config.py b/invenio/modules/workflows/config.py deleted file mode 100644 index 500725647..000000000 --- a/invenio/modules/workflows/config.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -## This file is part of Invenio. -## Copyright (C) 2012, 2013 CERN. -## -## Invenio is free software; you can redistribute it and/or -## modify it under the terms of the GNU General Public License as -## published by the Free Software Foundation; either version 2 of the -## License, or (at your option) any later version. -## -## Invenio is distributed in the hope that it will be useful, but -## WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -## General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Invenio; if not, write to the Free Software Foundation, Inc., -## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. - -"""Invenio BibWorkflow config.""" - - -def enum(**enums): - return type('Enum', (), enums) - -CFG_WORKFLOW_STATUS = enum(NEW=0, RUNNING=1, HALTED=2, ERROR=3, - FINISHED=4, COMPLETED=5) -CFG_OBJECT_VERSION = enum(INITIAL=0, FINAL=1, HALTED=2, RUNNING=3) -CFG_OBJECT_STATUS = enum(ERROR="ERROR - Something went wrong!", - RUNNING="RUNNING - Workflow in process", - FINISHED="FINISHED - Workflow was finished" + - "for this object" - ) -CFG_LOG_TYPE = enum(INFO=0, ERROR=1, DEBUG=2) -CFG_EXTRA_DATA_KEY = enum(PUBLISHER=0, SOURCE=1, OWNER=2, - CATEGORY=3, LAST_TASK_NAME=4) diff --git a/invenio/modules/workflows/containers.py b/invenio/modules/workflows/containers.py index 3fa338091..737241768 100644 --- a/invenio/modules/workflows/containers.py +++ b/invenio/modules/workflows/containers.py @@ -1,88 +1,80 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -from .config import CFG_OBJECT_VERSION +from .models import ObjectVersion from .loader import widgets def create_hp_containers(iSortCol_0=None, sSortDir_0=None, - sSearch=None, version_showing=[CFG_OBJECT_VERSION.HALTED]): + sSearch=None, version_showing=[ObjectVersion.HALTED], + type_showing=[]): """ Looks for related HPItems and groups them together in HPContainers @type hpitems: list @return: A list containing all the HPContainers. """ from .models import BibWorkflowObject if iSortCol_0: iSortCol_0 = int(iSortCol_0) - print '************************version_showing', version_showing - bwobject_list = BibWorkflowObject.query.filter( - BibWorkflowObject.id_parent != 0 and \ - BibWorkflowObject.version.in_(version_showing)).all() + BibWorkflowObject.id_parent != 0 and + not version_showing or BibWorkflowObject.version.in_(version_showing) + ).all() - print 'GOT THAT MANY RECORDS HERE:', len(bwobject_list) if sSearch: - print sSearch if len(sSearch) < 4: pass else: bwobject_list_tmp = [] for bwo in bwobject_list: extra_data = bwo.get_extra_data() if bwo.id_parent == sSearch: bwobject_list_tmp.append(bwo) elif bwo.id_user == sSearch: bwobject_list_tmp.append(bwo) elif bwo.id_workflow == sSearch: bwobject_list_tmp.append(bwo) elif extra_data['_last_task_name'] == sSearch: bwobject_list_tmp.append(bwo) else: try: - widget = widgets[extra_data['widget']] - if sSearch in widget.__title__ or sSearch in extra_data['widget']: + widget_name = bwo.get_widget() + widget = widgets[widget_name] + if sSearch in widget.__title__ or sSearch in widget_name: bwobject_list_tmp.append(bwo) except: pass try: if sSearch in extra_data['redis_search']['category']: bwobject_list_tmp.append(bwo) elif sSearch in extra_data['redis_search']['source']: bwobject_list_tmp.append(bwo) elif sSearch in extra_data['redis_search']['title']: bwobject_list_tmp.append(bwo) except KeyError: pass bwobject_list = bwobject_list_tmp if iSortCol_0 == -6: if sSortDir_0 == 'desc': bwobject_list.reverse() - - return bwobject_list -try: - bwolist = create_hp_containers(version_showing=current_app.config['VERSION_SHOWING']) - print "try succeded" -except: - print "try failed" - bwolist = create_hp_containers() + return bwobject_list diff --git a/invenio/modules/workflows/engine.py b/invenio/modules/workflows/engine.py index 92391d3d1..cc2c41423 100644 --- a/invenio/modules/workflows/engine.py +++ b/invenio/modules/workflows/engine.py @@ -1,449 +1,475 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. from six.moves import cPickle import sys from datetime import datetime from six import iteritems from uuid import uuid1 as new_uuid import base64 from workflow.engine import (GenericWorkflowEngine, ContinueNextToken, HaltProcessing, StopProcessing, JumpTokenBack, JumpTokenForward, WorkflowError) from invenio.ext.sqlalchemy import db from invenio.config import CFG_DEVEL_SITE from .models import (Workflow, BibWorkflowObject, - BibWorkflowEngineLog) -from .utils import dictproperty, get_workflow_definition -from .config import (CFG_WORKFLOW_STATUS, - CFG_OBJECT_VERSION) + BibWorkflowEngineLog, + ObjectVersion) +from .utils import (dictproperty, + get_workflow_definition) from .logger import (get_logger, BibWorkflowLogHandler) from .errors import WorkflowHalt DEBUG = CFG_DEVEL_SITE > 0 +class WorkflowStatus(object): + NEW, RUNNING, HALTED, ERROR, FINISHED, COMPLETED = range(6) + + class BibWorkflowEngine(GenericWorkflowEngine): + """ + Subclass of GenericWorkflowEngine representing a workflow in + the workflows module. + + Adds a SQLAlchemy database model to save workflow states and + workflow data. + + Overrides key functions in GenericWorkflowEngine to implement + logging and certain workarounds for storing data before/after + task calls (This part will be revisited in the future). + """ def __init__(self, name=None, uuid=None, curr_obj=0, workflow_object=None, id_user=0, module_name="Unknown", **kwargs): super(BibWorkflowEngine, self).__init__() self.db_obj = None if isinstance(workflow_object, Workflow): self.db_obj = workflow_object else: # If uuid is defined we try to get the db object from DB. if uuid is not None: self.db_obj = \ Workflow.get(Workflow.uuid == uuid).first() else: uuid = new_uuid() if self.db_obj is None: self.db_obj = Workflow(name=name, id_user=id_user, current_object=curr_obj, module_name=module_name, uuid=uuid) self._create_db_obj() db_handler_obj = BibWorkflowLogHandler(BibWorkflowEngineLog, "uuid") self.log = get_logger(logger_name="workflow.%s" % self.db_obj.uuid, db_handler_obj=db_handler_obj, obj=self) self.set_workflow_by_name(self.name) self.set_extra_data_params(**kwargs) def get_extra_data(self): """ Main method to retrieve data saved to the object. """ return cPickle.loads(base64.b64decode(self.db_obj._extra_data)) def set_extra_data(self, value): """ Main method to update data saved to the object. """ self.db_obj._extra_data = base64.b64encode(cPickle.dumps(value)) def extra_data_get(self, key): if key not in self.db_obj.get_extra_data(): raise KeyError("%s not in extra_data" % (key,)) return self.db_obj.get_extra_data()[key] def extra_data_set(self, key, value): tmp = self.db_obj.get_extra_data() tmp[key] = value self.db_obj.set_extra_data(tmp) extra_data = dictproperty(fget=extra_data_get, fset=extra_data_set, doc="Sets up property") del extra_data_get, extra_data_set @property def counter_object(self): return self.db_obj.counter_object @property def uuid(self): return self.db_obj.uuid @property def id_user(self): return self.db_obj.id_user @property def module_name(self): return self.db_obj.module_name @property def name(self): return self.db_obj.name @property def status(self): return self.db_obj.status def __getstate__(self): if not self._picklable_safe: raise cPickle.PickleError("The instance of the workflow engine " "cannot be serialized, " "because it was constructed with " "custom, user-supplied callbacks. " "Either use PickableWorkflowEngine or " "provide your own __getstate__ method.") state = self.__dict__.copy() del state['log'] return state def __setstate__(self, state): if len(self._objects) < self._i[0]: raise cPickle.PickleError("The workflow instance " "inconsistent state, " "too few objects") db_handler_obj = BibWorkflowLogHandler(BibWorkflowEngineLog, "uuid") state['log'] = get_logger(logger_name="workflow.%s" % state['uuid'], db_handler_obj=db_handler_obj, obj=self) self.__dict__ = state def __repr__(self): return "<BibWorkflow_engine(%s)>" % (self.db_obj.name,) def __str__(self, log=False): return """------------------------------- BibWorkflowEngine ------------------------------- %s ------------------------------- """ % (self.db_obj.__str__(),) @staticmethod def before_processing(objects, self): """ Executed before processing the workflow. """ # 1. Save workflow (ourselves). if not self.db_obj.uuid: self.save() self.set_counter_initial(len(objects)) self.log.info("Workflow has been started") # 2. We want to save all the objects as version 0. for obj in objects: same_workflow = \ obj.id_workflow and \ obj.id_workflow == self.db_obj.uuid if obj.id and same_workflow: # If object exists and we are running the same workflow, # do nothing obj.log.info("object saving process : was already existing") continue # Set the current workflow id in the object - if obj.version == CFG_OBJECT_VERSION.INITIAL \ + if obj.version == ObjectVersion.INITIAL \ and obj.id_workflow is not None: obj.log.info("object saving process : was already existing") pass else: obj.id_workflow = self.uuid obj.save(obj.version) GenericWorkflowEngine.before_processing(objects, self) @staticmethod def after_processing(objects, self): self._i = [-1, [0]] if self.has_completed(): - self.save(CFG_WORKFLOW_STATUS.COMPLETED) + self.save(WorkflowStatus.COMPLETED) else: - self.save(CFG_WORKFLOW_STATUS.FINISHED) + self.save(WorkflowStatus.FINISHED) def _create_db_obj(self): db.session.add(self.db_obj) db.session.commit() self.log.info("Workflow saved to db as new object.") def _update_db(self): db.session.commit() self.log.info("Workflow saved to db.") def has_completed(self): """ Returns True of workflow is fully completed meaning that all associated objects are in FINAL state. """ number_of_objects = BibWorkflowObject.query.filter( BibWorkflowObject.id_workflow == self.uuid, - BibWorkflowObject.version.in_([CFG_OBJECT_VERSION.HALTED, - CFG_OBJECT_VERSION.RUNNING]) + BibWorkflowObject.version.in_([ObjectVersion.HALTED, + ObjectVersion.RUNNING]) ).count() return number_of_objects == 0 - def save(self, status=CFG_WORKFLOW_STATUS.NEW): + def save(self, status=WorkflowStatus.NEW): """ Save the workflow instance to database. Just storing the necessary data. No serialization (!). Status: 0 - new, 1 - running, 2 - halted, 3 - error, 4 - finished """ if not self.db_obj.uuid: # We do not have an ID, # so we need to add ourselves (first execution). self._create_db_obj() else: # This workflow continues a previous execution. - if status in (CFG_WORKFLOW_STATUS.FINISHED, - CFG_WORKFLOW_STATUS.HALTED): + if status in (WorkflowStatus.FINISHED, + WorkflowStatus.HALTED): self.db_obj.current_object = 0 self.db_obj.modified = datetime.now() self.db_obj.status = status self._update_db() def process(self, objects): super(BibWorkflowEngine, self).process(objects) def restart(self, obj, task): """Restart the workflow engine after it was deserialized """ self.log.info("Restarting workflow from %s object and %s task" % (str(obj), str(task),)) # set the point from which to start processing if obj == 'prev': # start with the previous object self._i[0] -= 2 #TODO: check if there is any object there elif obj == 'current': # continue with the current object self._i[0] -= 1 elif obj == 'next': pass else: raise Exception('Unknown start point for object: %s' % obj) # set the task that will be executed first if task == 'prev': # the previous self._i[1][-1] -= 1 elif task == 'current': # restart the task again self._i[1][-1] -= 0 elif task == 'next': # continue with the next task self._i[1][-1] += 1 else: raise Exception('Unknown start pointfor task: %s' % obj) self.process(self._objects) self._unpickled = False @staticmethod def processing_factory(objects, self): """Default processing factory extended with saving objects before succesful processing. Default processing factory, will process objects in order @var objects: list of objects (passed in by self.process()) @keyword cls: engine object itself, because this method may be implemented by the standalone function, we pass the self also as a cls argument As the WFE proceeds, it increments the internal counter, the first position is the number of the element. This pointer increases before the object is taken 2nd pos is reserved for the array that points to the task position. The number there points to the task that is currently executed; when error happens, it will be there unchanged. The pointer is updated after the task finished running. """ self.before_processing(objects, self) i = self._i # negative index not allowed, -1 is special while len(objects) - 1 > i[0] >= -1: i[0] += 1 obj = objects[i[0]] obj.log.info("Object is selected for processing") callbacks = self.callback_chooser(obj, self) if callbacks: try: self.run_callbacks(callbacks, objects, obj) i[1] = [0] # reset the callbacks pointer except StopProcessing: if DEBUG: self.log.debug("Processing was stopped: '%s' " "(object: %s)" % (str(callbacks), repr(obj))) obj.log.debug("Processing has stopped") break except JumpTokenBack as step: if step.args[0] > 0: raise WorkflowError("JumpTokenBack cannot" " be positive number") if DEBUG: self.log.debug('Warning, we go back [%s] objects' % step.args[0]) obj.log.debug("Object preempted") i[0] = max(-1, i[0] - 1 + step.args[0]) i[1] = [0] # reset the callbacks pointer except JumpTokenForward as step: if step.args[0] < 0: raise WorkflowError("JumpTokenForward cannot" " be negative number") if DEBUG: self.log.debug('We skip [%s] objects' % step.args[0]) obj.log.debug("Object preempted") i[0] = min(len(objects), i[0] - 1 + step.args[0]) i[1] = [0] # reset the callbacks pointer except ContinueNextToken: if DEBUG: self.log.debug('Stop processing for this object, ' 'continue with next') obj.log.debug("Object preempted") i[1] = [0] # reset the callbacks pointer continue except HaltProcessing as e: self.increase_counter_halted() extra_data = obj.get_extra_data() obj.set_extra_data(extra_data) if DEBUG: self.log.info('Processing was halted at step: %s' % i) # reraise the exception, #this is the only case when a WFE can be completely # stopped obj.log.info("Object proccesing is halted") if type(e) == WorkflowHalt: raise e else: raise WorkflowHalt(e) except Exception as e: obj.log.error("Something terribly wrong happend to this object") extra_data = obj.get_extra_data() obj.set_extra_data(extra_data) raise # We save the object once it is fully run through - obj.save(CFG_OBJECT_VERSION.FINAL) + obj.save(ObjectVersion.FINAL) obj.log.info("Object proccesing is finished") self.increase_counter_finished() self.log.info("Done saving object: %i" % (obj.id, )) self.after_processing(objects, self) def execute_callback(self, callback, obj): """Executes the callback - override this method to implement logging""" obj.data = obj.get_data() obj.extra_data = obj.get_extra_data() obj.extra_data["_last_task_name"] = self.get_current_taskname() self.log.info(obj.extra_data["_last_task_name"]) self.extra_data = self.get_extra_data() try: callback(obj, self) finally: self.set_extra_data(self.extra_data) obj.set_data(obj.data) obj.set_extra_data(obj.extra_data) def get_current_taskname(self): """ Will attempt to return name of current task/step. Otherwise returns None. """ callback_list = self.getCallbacks() if callback_list: + self.log.info(str(self.getCallbacks())) + self.log.info(str(self.getCurrTaskId())) for i in self.getCurrTaskId(): callback_list = callback_list[i] return callback_list.func_name def get_current_object(self): """ Returns the currently active BibWorkflowObject or None if no object is active. """ obj_id = self.getCurrObjId() if obj_id < 0: return None return self._objects[obj_id] def halt(self, msg, widget=None): """Halt the workflow (stop also any parent wfe)""" raise WorkflowHalt(message=msg, widget=widget, id_workflow=self.uuid) + def get_default_data_type(self): + """ + Returns default data type from workflow + definition. + """ + return getattr(self.workflow_definition, + "object_type", + "") + def set_counter_initial(self, obj_count): self.db_obj.counter_initial = obj_count self.db_obj.counter_halted = 0 self.db_obj.counter_error = 0 self.db_obj.counter_finished = 0 def increase_counter_halted(self): self.db_obj.counter_halted += 1 def increase_counter_error(self): self.db_obj.counter_error += 1 def increase_counter_finished(self): self.db_obj.counter_finished += 1 def set_workflow_by_name(self, workflow_name): workflow = get_workflow_definition(workflow_name) self.workflow_definition = workflow self.setWorkflow(self.workflow_definition.workflow) def set_extra_data_params(self, **kwargs): tmp = self.get_extra_data() if not tmp: tmp = {} for key, value in iteritems(kwargs): tmp[key] = value self.set_extra_data(tmp) diff --git a/invenio/modules/workflows/errors.py b/invenio/modules/workflows/errors.py index 3f880ea25..408f510b7 100644 --- a/invenio/modules/workflows/errors.py +++ b/invenio/modules/workflows/errors.py @@ -1,136 +1,158 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ invenio.modules.workflows.errors ------------------------ Contains standard error messages for workflows module. """ from workflow.engine import HaltProcessing class WorkflowHalt(HaltProcessing): """ Raised when workflow should be halted. Also contains the widget to be displayed. """ def __init__(self, message, widget=None, **kwargs): HaltProcessing.__init__(self) self.message = message self.widget = widget self.payload = kwargs def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message rv['widget'] = self.widget return rv def __str__(self): """String representation.""" return "WorkflowHalt(%s, widget: %s, payload: %r)" % \ (repr(self.message), repr(self.widget), repr(self.payload)) # class WorkflowError(Exception): # """Raised when workflow experiences an error.""" # # def __init__(self, message, id_workflow=None, id_object=None, **kwargs): # self.message = message # self.id_workflow = id_workflow # self.id_object = id_object # self.payload = kwargs # super(WorkflowError, self).__init__(message, id_workflow, id_object, kwargs) # # def to_dict(self): # rv = dict(self.payload or ()) # rv['message'] = self.message # rv['id_workflow'] = self.id_workflow # rv['id_object'] = self.id_object # return rv # # def __str__(self): # """String representation.""" # return "WorkflowError(%s, id_workflow: %s, id_object: %s, payload: %r)" % \ # (self.message, str(self.id_workflow), str(self.id_object), repr(self.payload)) class WorkflowError(Exception): """Raised when workflow experiences an error.""" def __init__(self, message, id_workflow, id_object, payload=[]): self.message = message self.id_workflow = id_workflow self.id_object = id_object self.payload = payload Exception.__init__(self, message, message, id_object, payload) # <-- REQUIRED def to_dict(self): rv = list(self.payload or []) rv['message'] = self.message rv['id_workflow'] = self.id_workflow rv['id_object'] = self.id_object return rv def __str__(self): """String representation.""" return "WorkflowError(%s, id_workflow: %s, id_object: %s, payload: %r)" % \ (self.message, str(self.id_workflow), str(self.id_object), repr(self.payload)) + class WorkflowDefinitionError(Exception): """Raised when workflow definition is missing.""" def __init__(self, message, workflow_name, **kwargs): Exception.__init__(self) self.message = message self.workflow_name = workflow_name self.payload = kwargs def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message rv['workflow_name'] = self.workflow_name return rv def __str__(self): """String representation.""" return "WorkflowDefinitionError(%s, workflow_name: %s, payload: %r)" % \ (repr(self.message), self.workflow_name, repr(self.payload) or "None") class WorkflowWorkerError(Exception): """Raised when there is a problem with workflow workers.""" def __init__(self, message, worker_name, **kwargs): Exception.__init__(self) self.message = message self.worker_name = worker_name self.payload = kwargs def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message rv['worker_name'] = self.worker_name return rv def __str__(self): """String representation.""" return "WorkflowDefinitionError(%s, worker_name: %s, payload: %r)" % \ - (repr(self.message), self.worker_name, repr(self.payload) or "None") \ No newline at end of file + (repr(self.message), self.worker_name, repr(self.payload) or "None") + + +class WorkflowObjectVersionError(Exception): + """ Raised when workflow object has an unknown or missing version """ + + def __init__(self, message, id_object, obj_version): + self.message = message + self.obj_version = obj_version + self.id_object = id_object + + def to_dict(self): + rv = {} + rv['message'] = self.message + rv['obj_version'] = self.obj_version + rv['id_object'] = self.id_object + return rv + + def __str__(self): + """String representation.""" + return "WorkflowObjectVersionError(%s, obj_version: %s, id_object: %s)" % \ + (self.message, str(self.obj_version), str(self.id_object)) diff --git a/invenio/modules/workflows/hp_field_widgets.py b/invenio/modules/workflows/hp_field_widgets.py index 9c16d2c48..18d62bee7 100644 --- a/invenio/modules/workflows/hp_field_widgets.py +++ b/invenio/modules/workflows/hp_field_widgets.py @@ -1,81 +1,88 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2013, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. from wtforms.widgets import html_params, HTMLString -def bootstrap_submit(field): +def bootstrap_accept(field): + """ + Accept button for hp + """ html = u'<input %s >' \ % html_params(id="submitButton", - class_="btn btn-primary btn-lg", + class_="btn btn-success", name="submitButton", type="submit", value=field.label.text,) return HTMLString(u''.join(html)) -def bootstrap_accept(field): +def bootstrap_submit(field): """ - Accept button for hp + Submit button for edit record widget """ html = u'<input %s >' \ % html_params(id="submitButton", - class_="btn btn-success", + class_="btn btn-sm btn-primary", name="submitButton", - type="submit", - value=field.label.text,) + type="submit",) return HTMLString(u''.join(html)) -def bootstrap_accept_mini(field): + +def bootstrap_accept_mini(field, **kwargs): """ Mini Accept button for hp """ + objectid = kwargs.pop('objectid', '') html = u'<input %s >' \ % html_params(id="submitButtonMini", class_="btn btn-success btn-xs", name="submitButton", type="submit", value=field.label.text, - onclick="mini_approval('Accept', event);",) + onclick="mini_approval('Accept', event, %s);" % (objectid,),) return HTMLString(u''.join(html)) + def bootstrap_reject(field): """ Reject button for hp """ html = u'<input %s >' \ % html_params(id="submitButton", class_="btn btn-danger", name="submitButton", type="submit", value=field.label.text,) return HTMLString(u''.join(html)) -def bootstrap_reject_mini(field): + +def bootstrap_reject_mini(field, **kwargs): """ Mini Reject button for hp """ + objectid = kwargs.pop('objectid', '') html = u'<input %s >' \ % html_params(id="submitButtonMini", class_="btn btn-danger btn-xs", name="submitButton", type="submit", value=field.label.text, - onclick="mini_approval('Reject', event);",) + onclick="mini_approval('Reject', event, %s);" % (objectid,),) return HTMLString(u''.join(html)) diff --git a/invenio/modules/workflows/models.py b/invenio/modules/workflows/models.py index 061124485..de3adbcfb 100644 --- a/invenio/modules/workflows/models.py +++ b/invenio/modules/workflows/models.py @@ -1,574 +1,591 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. import os import tempfile from six.moves import cPickle import base64 import logging import six from datetime import datetime from sqlalchemy import desc from sqlalchemy.orm.exc import NoResultFound from invenio.ext.sqlalchemy import db from invenio.base.globals import cfg -from .config import CFG_OBJECT_VERSION -from .utils import redis_create_search_entry, WorkflowsTaskResult +from .utils import redis_create_search_entry, WorkflowsTaskResult from .logger import (get_logger, BibWorkflowLogHandler) +class ObjectVersion(object): + INITIAL = 0 + FINAL = 1 + HALTED = 2 + RUNNING = 3 + + def get_default_data(): """ Returns the base64 representation of the data default value """ data_default = {} return base64.b64encode(cPickle.dumps(data_default)) def get_default_extra_data(): """ Returns the base64 representation of the extra_data default value """ extra_data_default = {"_tasks_results": [], "owner": {}, - "task_counter": {}, + "_task_counter": {}, "error_msg": "", "_last_task_name": "", "latest_object": -1, "widget": None, - "redis_search": {}} + "redis_search": {}, + "source": ""} return base64.b64encode(cPickle.dumps(extra_data_default)) class Workflow(db.Model): __tablename__ = "bwlWORKFLOW" uuid = db.Column(db.String(36), primary_key=True, nullable=False) name = db.Column(db.String(255), default="Default workflow", nullable=False) created = db.Column(db.DateTime, default=datetime.now, nullable=False) modified = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, nullable=False) id_user = db.Column(db.Integer, default=0, nullable=False) - _extra_data = db.Column(db.LargeBinary, nullable=False, default=get_default_extra_data()) + _extra_data = db.Column(db.LargeBinary, + nullable=False, + default=get_default_extra_data()) status = db.Column(db.Integer, default=0, nullable=False) current_object = db.Column(db.Integer, default="0", nullable=False) objects = db.relationship("BibWorkflowObject", backref="bwlWORKFLOW") counter_initial = db.Column(db.Integer, default=0, nullable=False) counter_halted = db.Column(db.Integer, default=0, nullable=False) counter_error = db.Column(db.Integer, default=0, nullable=False) counter_finished = db.Column(db.Integer, default=0, nullable=False) module_name = db.Column(db.String(64), nullable=False) def __repr__(self): return "<Workflow(name: %s, module: %s, cre: %s, mod: %s," \ "id_user: %s, status: %s)>" % \ (str(self.name), str(self.module_name), str(self.created), str(self.modified), str(self.id_user), str(self.status)) def __str__(self): return """Workflow: Uuid: %s Name: %s User id: %s Module name: %s Created: %s Modified: %s Status: %s Current object: %s Counters: initial=%s, halted=%s, error=%s, finished=%s Extra data: %s""" % (str(self.uuid), str(self.name), str(self.id_user), str(self.module_name), str(self.created), str(self.modified), str(self.status), str(self.current_object), str(self.counter_initial), str(self.counter_halted), str(self.counter_error), str(self.counter_finished), str(self._extra_data),) @classmethod def get(cls, *criteria, **filters): """ A wrapper for the filter and filter_by functions of sqlalchemy. Define a dict with which columns should be filtered by which values. e.g. Workflow.get(uuid=uuid) Workflow.get(Workflow.uuid != uuid) The function supports also "hybrid" arguments. e.g. Workflow.get(Workflow.module_name != 'i_hate_this_module', user_id=user_id) look up also sqalchemy BaseQuery's filter and filter_by documentation """ return cls.query.filter(*criteria).filter_by(**filters) @classmethod def get_status(cls, uuid=None): """ Returns the status of the workflow """ return cls.get(Workflow.uuid == uuid).one().status @classmethod def get_most_recent(cls, *criteria, **filters): """ Returns the most recently modified workflow. """ most_recent = cls.get(*criteria, **filters).\ order_by(desc(Workflow.modified)).first() if most_recent is None: raise NoResultFound else: return most_recent @classmethod def get_objects(cls, uuid=None): """ Returns the objects of the workflow """ return cls.get(Workflow.uuid == uuid).one().objects def get_extra_data(self, user_id=0, uuid=None, key=None, getter=None): """Returns a json of the column extra_data or if any of the other arguments are defined, a specific value. You can define either the key or the getter function. @param key: the key to access the desirable value @param getter: a callable that takes a dict as param and returns a value """ extra_data = Workflow.get(Workflow.id_user == self.id_user, Workflow.uuid == self.uuid).one()._extra_data extra_data = cPickle.loads(base64.b64decode(extra_data)) if key is not None: return extra_data[key] elif callable(getter): return getter(extra_data) def set_extra_data(self, user_id=0, uuid=None, key=None, value=None, setter=None): """Modifies the json of the column extra_data or if any of the other arguments are defined, a specific value. You can define either the key, value or the setter function. @param key: the key to access the desirable value @param value: the new value @param setter: a callable that takes a dict as param and modifies it """ extra_data = Workflow.get(Workflow.id_user == user_id, Workflow.uuid == uuid).one()._extra_data extra_data = cPickle.loads(base64.b64decode(extra_data)) if key is not None and value is not None: extra_data[key] = value elif callable(setter): setter(extra_data) - Workflow.get(Workflow.uuid == self.uuid).update({'_extra_data': base64.b64encode(cPickle.dumps(extra_data))}) + Workflow.get(Workflow.uuid == self.uuid).update( + {'_extra_data': base64.b64encode(cPickle.dumps(extra_data))} + ) @classmethod def delete(cls, uuid=None): cls.get(Workflow.uuid == uuid).delete() db.session.commit() class BibWorkflowObject(db.Model): # db table definition __tablename__ = "bwlOBJECT" id = db.Column(db.Integer, primary_key=True) # Our internal data column. Default is encoded dict. - _data = db.Column(db.LargeBinary, nullable=False, default=get_default_data()) - _extra_data = db.Column(db.LargeBinary, nullable=False, default=get_default_extra_data()) + _data = db.Column(db.LargeBinary, + nullable=False, + default=get_default_data()) + _extra_data = db.Column(db.LargeBinary, + nullable=False, + default=get_default_extra_data()) id_workflow = db.Column(db.String(36), db.ForeignKey("bwlWORKFLOW.uuid"), nullable=True) version = db.Column(db.Integer(3), - default=CFG_OBJECT_VERSION.RUNNING, nullable=False) + default=ObjectVersion.RUNNING, nullable=False) id_parent = db.Column(db.Integer, db.ForeignKey("bwlOBJECT.id"), default=None) child_objects = db.relationship("BibWorkflowObject", remote_side=[id_parent]) created = db.Column(db.DateTime, default=datetime.now, nullable=False) modified = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, nullable=False) status = db.Column(db.String(255), default="", nullable=False) - persistent_ids = db.Column(db.JSON, default= {} ,nullable=True) - data_type = db.Column(db.String(150), default=DATA_TYPES.ANY, + persistent_ids = db.Column(db.JSON, default={}, nullable=True) + data_type = db.Column(db.String(150), default="", nullable=True) - uri = db.Column(db.String(500), default="") id_user = db.Column(db.Integer, default=0, nullable=False) child_logs = db.relationship("BibWorkflowObjectLog") workflow = db.relationship( Workflow, foreign_keys=[id_workflow], remote_side=Workflow.uuid ) _log = None @property def log(self): if not self._log: db_handler_obj = BibWorkflowLogHandler(BibWorkflowObjectLog, "id") - self._log = get_logger(logger_name="object.%s_%s" % (self.id_workflow, self.id), + self._log = get_logger(logger_name="object.%s_%s" % + (self.id_workflow, self.id), db_handler_obj=db_handler_obj, loglevel=logging.DEBUG, obj=self) return self._log def get_data(self): """ Main method to retrieve data saved to the object. """ return cPickle.loads(base64.b64decode(self._data)) def set_data(self, value): """ Main method to update data saved to the object. """ self._data = base64.b64encode(cPickle.dumps(value)) def get_extra_data(self): """ Main method to retrieve data saved to the object. """ return cPickle.loads(base64.b64decode(self._extra_data)) def set_extra_data(self, value): """ Main method to update data saved to the object. """ self._extra_data = base64.b64encode(cPickle.dumps(value)) def _create_db_obj(self): db.session.add(self) db.session.commit() def __repr__(self): return "<BibWorkflowObject(id = %s, data = %s, id_workflow = %s, " \ "version = %s, id_parent = %s, created = %s, extra_data = %s)" \ % (str(self.id), str(self.get_data()), str(self.id_workflow), str(self.version), str(self.id_parent), str(self.created), str(self.get_extra_data())) - def __str__(self, log=False): - return """ -------------------------------- -BibWorkflowObject -------------------------------- - Extra object class: - Self status: %s -------------------------------- - BibWorkflowObject: - - Id: %s - Parent id: %s - Workflow id: %s - Created: %s - Modified: %s - Version: %s - DB_obj status: %s - Data type: %s - URI: %s - Data: %s - Extra data: %s -------------------------------- -""" % (str(self.status), - str(self.id), - str(self.id_parent), - str(self.id_workflow), - str(self.created), - str(self.modified), - str(self.version), - str(self.status), - str(self.data_type), - str(self.uri), - str(self.get_data()), - str(self.get_extra_data),) - def __eq__(self, other): if isinstance(other, BibWorkflowObject): if self._data == other._data and \ self._extra_data == other._extra_data and \ self.id_workflow == other.id_workflow and \ self.version == other.version and \ self.id_parent == other.id_parent and \ isinstance(self.created, datetime) and \ isinstance(self.modified, datetime): return True else: return False return NotImplemented def __ne__(self, other): return not self.__eq__(other) def add_task_result(self, name, result): """ Adds given task results to extra_data in order to be accessed and displayed later on by Holding Pen templates. """ task_name = self.extra_data["_last_task_name"] res_obj = WorkflowsTaskResult(task_name, name, result) self.extra_data["_tasks_results"].append(res_obj) def add_widget(self, widget, message): + """ + Assign a widget to this object for an action to be taken + in holdingpen. The widget is reffered to by a string with + the filename minus extension. Ex: approval_widget. + + A message is also needed to tell the user the action + required. + """ extra_data = self.get_extra_data() extra_data["_widget"] = widget extra_data["_message"] = message self.set_extra_data(extra_data) + def get_widget(self): + """ + Retrive the currently assigned widget, if any. + """ + try: + return self.get_extra_data()["_widget"] + except KeyError: + # No widget + return None + def remove_widget(self): + """ + Removes the currently assigned widget. + """ extra_data = self.get_extra_data() extra_data["_widget"] = None extra_data["_message"] = "" self.set_extra_data(extra_data) def change_status(self, message): self.status = message def get_current_task(self): - return self.get_extra_data()["task_counter"] + """ + Returns the current progress structure from the workflow + engine for this object. + """ + extra_data = self.get_extra_data() + try: + return extra_data["_task_counter"] + except KeyError: + # Assume old version "task_counter" + return extra_data["task_counter"] def _create_version_obj(self, id_workflow, version, id_parent=None, no_update=False): obj = BibWorkflowObject(_data=self._data, id_workflow=id_workflow, version=version, id_parent=id_parent, _extra_data=self._extra_data, status=self.status, data_type=self.data_type) db.session.add(obj) db.session.commit() - if version is CFG_OBJECT_VERSION.INITIAL and not no_update: + if version is ObjectVersion.INITIAL and not no_update: self.id_parent = obj.id db.session.commit() return obj.id def _update_db(self): db.session.add(self) db.session.commit() def save(self, version=None, task_counter=[0], id_workflow=None): """ Saved object """ if not self.id: db.session.add(self) db.session.commit() extra_data = self.get_extra_data() extra_data["_task_counter"] = task_counter self.set_extra_data(extra_data) if not id_workflow: id_workflow = self.id_workflow if version: self.version = version - if version in (CFG_OBJECT_VERSION.FINAL, CFG_OBJECT_VERSION.HALTED): + if version in (ObjectVersion.FINAL, ObjectVersion.HALTED): redis_create_search_entry(self) self._update_db() def save_to_file(self, directory=None, prefix="workflow_object_data_", suffix=".obj"): """ Saves the contents of self.data['data'] to file. Returns path to saved file. Warning: Currently assumes non-binary content. """ if directory is None: directory = cfg['CFG_TMPSHAREDIR'] tmp_fd, filename = tempfile.mkstemp(dir=directory, prefix=prefix, suffix=suffix) os.write(tmp_fd, self.get_data()) os.close(tmp_fd) return filename def __getstate__(self): return self.__dict__ def __setstate__(self, state): self.__dict__ = state def copy(self, other): """Copies data and metadata except id and id_workflow""" self._data = other._data self._extra_data = other._extra_data self.version = other.version self.id_parent = other.id_parent self.created = other.created self.modified = other.modified self.status = other.status self.data_type = other.data_type self.uri = other.uri def get_formatted_data(self, format=None, formatter=None): """ Returns the data in some chewable format. """ from invenio.modules.records.api import Record from invenio.modules.formatter.engine import format_record data = self.get_data() if formatter: # A seperate formatter is supplied return formatter(data) if isinstance(data, dict): # Dicts are cool on its own, but maybe its SmartJson (record) try: new_dict_representation = Record(data) data = new_dict_representation.legacy_export_as_marc() except Exception as e: - raise e + # Maybe not, submission? + return data if isinstance(data, six.string_types): # Its a string type, lets try to convert if format: # We can try formatter! # If already XML, format_record does not like it. if format != 'xm': try: return format_record(recID=None, of=format, xml_record=data) except TypeError as e: # Wrong kind of type pass else: # So, XML then from xml.dom.minidom import parseString try: pretty_data = parseString(data) return pretty_data.toprettyxml() except TypeError: # Probably not proper XML string then return "Data cannot be parsed: %s" % (data,) except Exception: # Some other parsing error pass # Just return raw string return data # Not any of the above types. How juicy! return data + @classmethod + def delete(cls, oid): + cls.get(BibWorkflowObject.id == oid).delete() + db.session.commit() + class BibWorkflowObjectLog(db.Model): """ This class represent a record of a log emit by an object into the database the object must be saved before using this class. Indeed it needs the id of the object into the database. """ __tablename__ = 'bwlOBJECTLOGGING' id = db.Column(db.Integer, primary_key=True) id_object = db.Column(db.Integer(255), db.ForeignKey('bwlOBJECT.id'), nullable=False) log_type = db.Column(db.Integer, default=0, nullable=False) created = db.Column(db.DateTime, default=datetime.now) message = db.Column(db.TEXT, default="", nullable=False) def __repr__(self): return "<BibWorkflowObjectLog(%i, %s, %s, %s)>" % \ (self.id, self.id_object, self.message, self.created) @classmethod def get(cls, *criteria, **filters): """ A wrapper for the filter and filter_by functions of sqlalchemy. Define a dict with which columns should be filtered by which values. look up also sqalchemy BaseQuery's filter and filter_by documentation """ return cls.query.filter(*criteria).filter_by(**filters) @classmethod def get_most_recent(cls, *criteria, **filters): """ Returns the most recently created log. """ most_recent = cls.get(*criteria, **filters).\ order_by(desc(BibWorkflowObjectLog.created)).first() if most_recent is None: raise NoResultFound else: return most_recent @classmethod def delete(cls, id=None): cls.get(BibWorkflowObjectLog.id == id).delete() db.session.commit() class BibWorkflowEngineLog(db.Model): __tablename__ = "bwlWORKFLOWLOGGING" id = db.Column(db.Integer, primary_key=True) id_object = db.Column(db.String(255), nullable=False) log_type = db.Column(db.Integer, default=0, nullable=False) created = db.Column(db.DateTime, default=datetime.now) message = db.Column(db.TEXT, default="", nullable=False) def __repr__(self): return "<BibWorkflowEngineLog(%i, %s, %s, %s)>" % \ (self.id, self.id_object, self.message, self.created) @classmethod def get(cls, *criteria, **filters): """ A wrapper for the filter and filter_by functions of sqlalchemy. Define a dict with which columns should be filtered by which values. look up also sqalchemy BaseQuery's filter and filter_by documentation """ return cls.query.filter(*criteria).filter_by(**filters) @classmethod def get_most_recent(cls, *criteria, **filters): """ Returns the most recently created log. """ most_recent = cls.get(*criteria, **filters).\ order_by(desc(BibWorkflowEngineLog.created)).first() if most_recent is None: raise NoResultFound else: return most_recent @classmethod def delete(cls, uuid=None): cls.get(BibWorkflowEngineLog.id == uuid).delete() db.session.commit() -__all__ = ['Workflow', 'BibWorkflowObject', 'BibWorkflowObjectLog', 'BibWorkflowEngineLog'] +__all__ = ['Workflow', 'BibWorkflowObject', + 'BibWorkflowObjectLog', 'BibWorkflowEngineLog'] diff --git a/invenio/modules/workflows/static/css/workflows/style.css b/invenio/modules/workflows/static/css/workflows/style.css index 13073c7db..f918d5efd 100644 --- a/invenio/modules/workflows/static/css/workflows/style.css +++ b/invenio/modules/workflows/static/css/workflows/style.css @@ -1,250 +1,276 @@ span.glyphicon-link { font-size: 1.2em; } + +.editField{ + width: 50px; +} + +.dropdown-headline { + font-weight: bold; +} + +#slide-down-menu{ + background-color:black; + opacity: 75%; + width:400px; + height:100px; + margin:0 auto; +} + +.slide-down-btns{ + +} .mini-approval-btn{ text-decoration: none; color: #fff; } .mini-approval-btn:hover{ text-decoration: none; color: #fff } +.maintablerowhover{ + background-color: #ffa; +} + .active2 { background-color:rgba(255,255,255,0.7); position:absolute; float:left; top:0; left:0; width:100%; height:100%; } .tag-alert{ width: 120px; padding: 4px; - font-size: 14px; + font-size: 12px; + font-weight: bold; + margin: 10px; } + .close-btn:hover{ text-decoration:none; cursor:pointer; } #goodbye-msg{ /*text-align: center;*/ font-size: 36px; font-color:green; } #datatables-top{ background-color: #FF2; } .details_link{ padding-bottom: 5px; } #example tbody tr.even:hover, #example tbody tr.odd:hover { background-color: #FFFFCC; } #example tbody tr td { user-select: none; /* CSS3 */ -moz-user-select: none; /* Gecko (Firefox) */ -khtml-user-select:none; /* Webkit (Safari, Chrome) */ } .task-btns{ padding-bottom:5px; } #accept-btn-bibmatch{ width: 100px; margin: 0 auto; padding-top: 5px; } .decision-btns{ width:500px; padding-top: 10px; margin: 0 auto; } #usermessage{ margin-top: 10px; text-align: center; } #successmessage{ margin-top: 10px; text-align: center; } #record_preview{ position: fixed; width: 550px; height: 700px; overflow: auto; } #match_preview{ width: 550px; height: 700px; overflow: auto; } .object_preview_container{ padding-top: 10px; padding-left: 50px; width: 750px; - height: 700px; overflow: auto; } #object_preview_container{ padding-top: 10px; padding-left: 50px; width: 750px; height: 700px; overflow: auto; } #entry_detiles > span { position:absolute; right:11%; cursor:pointer; } .entry_message_button { display:block; background: orange url("images/icons/next.png") no-repeat 110px; width:120px; height:15px; border-radius:10px; padding:5px 0 5px 10px; } .entry_message_button_back { display:block; background: white url("images/icons/prev.png") no-repeat 10px; border: 1px gray solid; width:80px; height:15px; border-radius:10px; padding:5px 0 5px 60px; } #entry_info { width: 70%; height: 65px; float: left; } #entry_detiles h1, #workflow_detiles h1{ font-size:18px; } #entry_detiles h2, #workflow_detiles h2{ font-size:16px; } #entry_detiles, #workflow_detiles { font-size: 12px; } #entry_metadata { float: left; width: 30%; height: 35px; text-align: right; padding-top: 20px; } #actions { float: right; width: 480px; overflow-y:auto; height:450px; } .entry_message { background-color: lightYellow; border: solid 1px; padding: 5px; margin-bottom: 10px; } #entry_preview { text-align:right; display:block; clear:left; border:1px gray solid; width:300px; height:450px; } #entry_preview textarea, #workflow_tasks textarea, #workflow_preview textarea{ width:100%; min-height:14em; } tr.child_workflow { background-color: lightgray; font-size: 12px; } #preview_type { position:absolute; top:52? 0px; } #preview_type button{ font-size:10px; } #preview_type .other_btn{ margin-left:10px; } .widget_list { background-color:red; margin:5px; } .widget_list span { display:block; width:30px; height:30px; background-color:white; float: left; margin-right: 8px; } #entry_keywords { float: left; margin:10px 0 0 0; width:600px; height:75px; } #action_buttons { margin:40px 0 0 0; width:150px; float:right; } #action_buttons #extra{ width:150px; } /****** bootstrap CSS hacks *******/ #myModal { margin-top: -350px; width: 80%; left: 25%; } .modal-body { max-height:600px; overflow:none; } /****** tinybox ******/ .tbox {position:absolute; display:none; padding:14px 17px; z-index:900} .tinner {padding:15px; -moz-border-radius:5px; border-radius:5px; background:#fff url(images/preload.gif) no-repeat 50% 50%; border-right:1px solid #333; border-bottom:1px solid #333} .tmask {position:absolute; display:none; top:0px; left:0px; height:100%; width:100%; background:#000; z-index:800} .tclose {position:absolute; top:0px; right:0px; width:30px; height:30px; cursor:pointer; background:url(images/close.png) no-repeat} .tclose:hover {background-position:0 -30px} #error {background:#ff6969; color:#fff; text-shadow:1px 1px #cf5454; border-right:1px solid #000; border-bottom:1px solid #000; padding:0} #error .tcontent {padding:10px 14px 11px; border:1px solid #ffb8b8; -moz-border-radius:5px; border-radius:5px} #success {background:#2ea125; color:#fff; text-shadow:1px 1px #1b6116; border-right:1px solid #000; border-bottom:1px solid #000; padding:10; -moz-border-radius:0; border-radius:0} #notification {background:lightgreen; color:black; border-right:1px solid #000; border-bottom:1px solid #000; padding:0} #frameless {padding:0} #frameless .tclose {left:6px} diff --git a/invenio/modules/workflows/static/js/workflows/entry_details.js b/invenio/modules/workflows/static/js/workflows/entry_details.js index c4d4ea1b9..00f213958 100644 --- a/invenio/modules/workflows/static/js/workflows/entry_details.js +++ b/invenio/modules/workflows/static/js/workflows/entry_details.js @@ -1,38 +1,38 @@ /* * This file is part of Invenio. * Copyright (C) 2013 CERN. * * Invenio is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Invenio is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Invenio; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ - -function bind_object_preview(url_prefix, entry_id) { - $("div.btn-group[name='object_preview_btn']").bind('click', function(event){ - var format = event.target.name; - jQuery.ajax({ - url: url_prefix, - data: {'oid': entry_id, - 'format': format}, - success: function(json){ - if(format == 'xm' || format == 'marcxml'){ - $('div[name="object_preview"]').wrapAll('<debug>').text(json); - }else{ - $('div[name="object_preview"]').html(json); +function($){ + function bind_object_preview(url_prefix, entry_id) { + $("div.btn-group[name='object_preview_btn']").bind('click', function(event){ + var format = event.target.name; + jQuery.ajax({ + url: url_prefix, + data: {'oid': entry_id, + 'format': format}, + success: function(json){ + if(format == 'xm' || format == 'marcxml'){ + $('div[name="object_preview"]').wrapAll('<debug>').text(json); + }else{ + $('div[name="object_preview"]').html(json); + } } - } - }) - }); -} + }) + }); + } diff --git a/invenio/modules/workflows/static/js/workflows/hp_datapreview.js b/invenio/modules/workflows/static/js/workflows/hp_datapreview.js index 6b9a88cce..afbc897fa 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_datapreview.js +++ b/invenio/modules/workflows/static/js/workflows/hp_datapreview.js @@ -1,43 +1,43 @@ /* * This file is part of Invenio. * Copyright (C) 2013 CERN. * * Invenio is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Invenio is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Invenio; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ function data_preview(url_preview, bwoid, format) { jQuery.ajax({ url: url_preview, data: {'oid': bwoid, - 'recformat': format}, + 'of': format}, success: function(json){ if(format == "xm" || format == "marcxml"){ if( json.data === ""){ json.data = "Preview not available"; } $('div[id="object_preview_container'+bwoid+'"]').empty(); $('div[id="object_preview_container'+bwoid+'"]').append("<pre><code id='object_preview' class='language-markup'></code></pre>"); $('code[id="object_preview"]').append(json.data); Prism.highlightElement($('code[id="object_preview"]')[0]); }else{ if( json.data === ""){ json.data = "Preview not available"; } $('div[id="object_preview_container'+bwoid+'"]').empty(); $('div[id="object_preview_container'+bwoid+'"]').append(json.data); } } }); -}; +}; \ No newline at end of file diff --git a/invenio/modules/workflows/static/js/workflows/hp_details.js b/invenio/modules/workflows/static/js/workflows/hp_details.js index b8d86ebd2..358f8c88e 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_details.js +++ b/invenio/modules/workflows/static/js/workflows/hp_details.js @@ -1,74 +1,97 @@ // -*- coding: utf-8 -*- // This file is part of Invenio. // Copyright (C) 2013 CERN. // // Invenio is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // Invenio is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Invenio; if not, write to the Free Software Foundation, Inc., // 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +url = new Object(); +var bwoid; + +function init_url_details(url_, bwoid_){ + url = url_; + bwoid = bwoid_; +} -function action_buttons (url_restart_record, url_restart_record_prev, url_continue) { + +function action_buttons(url, bwoid) { $('#restart_button').on('click', function() { - bwo_id = $(this).attr('name'); - console.log(bwo_id); jQuery.ajax({ - url: url_restart_record, - data: bwo_id, + url: url.url_restart_record, + data: {'bwobject_id': bwoid}, success: function(json){ bootstrap_alert('Object restarted'); } }); }); $('#restart_button_prev').on('click', function() { - bwo_id = $(this).attr('name'); - console.log(bwo_id); + console.log(bwoid); jQuery.ajax({ - url: url_restart_record_prev, - data: bwo_id, + url: url.url_restart_record_prev, + data: {'bwobject_id': bwoid}, success: function(json){ bootstrap_alert('Object restarted from previous task'); } }); }); $('#continue_button').on('click', function() { - bwo_id = $(this).attr('name'); - console.log(bwo_id); jQuery.ajax({ - url: url_continue, - data: bwo_id, + url: url.url_continue, + data: {'bwobject_id': bwoid}, success: function(json){ bootstrap_alert('Object continued from next task'); } }); }); -} -// function bootstrap_alert(message) { -// $('#alert_placeholder').html('<div class="alert"><a class="close" data-dismiss="alert">×</a><span>'+message+'</span></div>'); -// } + $('#edit_form').on('submit', function(event){ + event.preventDefault(); + var form_data = new Object; + $("#edit_form input").each(function() { + console.log($(this)[0].name); + if($(this)[0].name != 'submitButton'){ + if($(this)[0].name == 'core'){ + form_data[$(this)[0].name] = $(this)[0].checked; + } + else{ + form_data[$(this)[0].name] = $(this)[0].value; + } + } + }); + + console.log(form_data); + jQuery.ajax({ + type: 'POST', + url: url.url_resolve_edit, + data: {'objectid': bwoid, + 'data': form_data}, + success: function(json){ + bootstrap_alert('Record successfully edited'); + } + }); + }); +} -// window.setTimeout(function() { -// $("#alert_placeholder").fadeTo(500, 0).slideUp(500, function(){ -// }); -// }, 2000); +function bootstrap_alert(message) { + $('#alert_placeholder').html('<div class="alert"><a class="close" data-dismiss="alert">×</a><span>'+message+'</span></div>'); +} if ( window.addEventListener ) { $("div.btn-group[name='data_version']").bind('click', function(event){ version = event.target.name; }); -} - -// bwoid = getURLParameter('bwobject_id'); \ No newline at end of file +} \ No newline at end of file diff --git a/invenio/modules/workflows/static/js/workflows/hp_maintable.js b/invenio/modules/workflows/static/js/workflows/hp_maintable.js index d4bc956f1..1bb0bd05c 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_maintable.js +++ b/invenio/modules/workflows/static/js/workflows/hp_maintable.js @@ -1,413 +1,486 @@ /* * This file is part of Invenio. * Copyright (C) 2013 CERN. * * Invenio is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Invenio is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Invenio; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ var oTable; var selectedRow; var rowList = []; var rowIndexList = []; var hoveredRow = -1; var tagList = []; var recordsToApprove = []; var defaultcss='#example tbody tr.even:hover, #example tbody tr.odd:hover {background-color: #FFFFCC;}'; url = new Object(); function init_urls(url_) { url.load_table = url_.load_table; url.batch_widget = url_.batch_widget; url.resolve_widget = url_.resolve_widget; url.delete_single = url_.delete_single; url.refresh = url_.refresh; url.widget = url_.widget; url.details = url_.details; - - init_datatable(); } -function init_datatable(){ - oTable = $('#example').dataTable({ +function init_datatable(version_showing){ + oSettings = { "sDom": 'lf<"clear">rtip', "bJQueryUI": true, "bProcessing": true, "bServerSide": true, "bDestroy": true, "sAjaxSource": url.load_table, "oColVis": { "buttonText": "Select Columns", "bRestore": true, "sAlign": "left", "iOverlayFade": 1 }, - "aoColumnDefs":[{'bSortable': false, 'aTargets': [0]}], + "aoColumnDefs":[{'bSortable': false, 'aTargets': [1]}, + {'bSearchable': false, 'bVisible': false, 'aTargets': [0]}, + {'sWidth': "25%", 'aTargets': [2]}, + {'sWidth': "15%", 'aTargets': [4]}], "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) { - rememberSelected(nRow); + var id = aData[0]; + rememberSelected(nRow, id); oSettings = oTable.fnSettings(); + nRow.row_id = id; + nRow.checkbox = nRow.cells[0].firstChild; nRow.addEventListener("click", function(e) { selectRow(nRow, e, oSettings); }); + }, + "fnDrawCallback": function(){ + $('table#maintable td').bind('mouseenter', function () { + $(this).parent().children().each(function() { + $(this).addClass('maintablerowhover'); + }); + }); + $('table#maintable td').bind('mouseleave', function () { + $(this).parent().children().each(function() { + $(this).removeClass('maintablerowhover'); + }); + }); } + }; + oTable = $('#maintable').dataTable(oSettings); + oTable.on('page', function( e, o) { + $('#select-all')[0].checked = false; }); - oSettings = oTable.fnSettings(); + initialize_versions(version_showing); + $('.dropdown-toggle').dropdown(); + return oTable; - // $('#version-halted').click(); } -$('#batch_btn').on('click', function() { - if (rowList.length >= 1){ - var rowList_out = JSON.stringify(rowList); - console.log(rowList_out); - window.location = url.batch_widget + "?bwolist=" + rowList_out; - $(this).prop("disabled", true); - return false; - } -}); - -$('#refresh_button').on('click', function() { - jQuery.ajax({ - url: url.refresh, - success: function(json){ +// $('#refresh_button').on('click', function() { +// jQuery.ajax({ +// url: url.refresh, +// success: function(json){ - } - }); - oTable.fnDraw(false); -}); +// } +// }); +// oTable.fnDraw(false); +// }); // DataTables row selection functions //*********************************** $("#select-all").on("click", function(){ - selectAll(); + console.log($(this)[0].checked); + if($(this)[0].checked == true){ + selectAll(); + } + else{ + deselectAllFromPage(); + } }) function hoverRow(row) { row.style.background = "#FFFFEE"; } function unhoverRow(row) { - if($.inArray(selectCellByTitle(row, 'Id').innerText, rowList) > -1){ + console.log(row.row_id); + if($.inArray(row.row_id, rowList) > -1){ row.style.background = "#ffa"; } else{ row.style.cssText = defaultcss; } } function removeSelection () { if (window.getSelection) { // all browsers, except IE before version 9 var selection = window.getSelection (); selection.removeAllRanges (); } else { if (document.selection.createRange) { // Internet Explorer var range = document.selection.createRange (); document.selection.empty (); } } } function selectRange(row){ var toPos = oTable.fnGetPosition(row) + oSettings._iDisplayStart; var fromPos = rowIndexList[rowIndexList.length-1]; var i; + var current_row = null; if (toPos > fromPos){ for (i=fromPos; i<=toPos; i++){ j = i % 10; if($.inArray(i, rowIndexList) <= -1){ - if (selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[j]].nTr, 'Actions').innerText != 'N/A'){ + current_row = oSettings.aoData[oSettings.aiDisplay[j]].nTr; + if (selectCellByTitle(current_row, 'Actions').innerText != 'N/A'){ rowIndexList.push(i); - rowList.push(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[j]].nTr, 'Id').innerText); - oSettings.aoData[oSettings.aiDisplay[j]].nTr.style.background = "#ffa"; - checkbox = selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[j]].nTr, "").childNodes[1]; - checkbox.checked = true; + rowList.push(current_row.row_id); + current_row.style.background = "#ffa"; + current_row.checkbox.checked = true; } } } } else{ for (i=fromPos; i>=toPos; i--){ j = i % 10; if($.inArray(i, rowIndexList) <= -1){ - console.log(oSettings.aoData[oSettings.aiDisplay[j]].nTr); - if (selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[j]].nTr, 'Actions').innerText != 'N/A'){ + current_row = oSettings.aoData[oSettings.aiDisplay[j]].nTr + if (selectCellByTitle(current_row, 'Actions').innerText != 'N/A'){ rowIndexList.push(i); - rowList.push(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[j]].nTr, 'Id').innerText); - oSettings.aoData[oSettings.aiDisplay[j]].nTr.style.background = "#ffa"; - checkbox = selectCellByTitle(oSettings.aiDisplay[j].nTr, "").childNodes[1]; - checkbox.checked = false; + rowList.push(current_row.row_id); + current_row.style.background = "#ffa"; + current_row.checkbox.checked = false; } } } } document.getSelection().removeAllRanges(); } function selectAll(){ - console.log("selecting all"); - var toPos = oSettings._iDisplayEnd - 1; - var fromPos = 0; - + var fromPos = oSettings._iDisplayStart; + var toPos = oSettings._iDisplayLength-1 + fromPos; + var j; + + console.log(Object.keys(oSettings)); + console.log(oSettings._iDisplayLength); + console.log(oSettings._iRecordsTotal); + var current_row = null; for (var i=fromPos; i<=toPos; i++){ - if($.inArray(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[i]].nTr, 'Id').innerText, rowList) <= -1){ - if (selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[i]].nTr, 'Actions').innerText != 'N/A'){ + j = i%10; + current_row = oSettings.aoData[oSettings.aiDisplay[j]].nTr; + if($.inArray(current_row.row_id, rowList) <= -1){ + if (selectCellByTitle(current_row, 'Actions').innerText != 'N/A'){ rowIndexList.push(i); - rowList.push(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[i]].nTr, 'Id').innerText); - oSettings.aoData[oSettings.aiDisplay[i]].nTr.style.background = "#ffa"; + rowList.push(current_row.row_id); + current_row.style.background = "#ffa"; + current_row.cells[0].firstChild.checked = true; } } } } -function rememberSelected(row) { - selectedRow = row; - if($.inArray($.trim(selectCellByTitle(row, 'Id').innerText), rowList) > -1){ - selectedRow.style.background = "#ffa"; - selectedRow.cells[0].childNodes[1].checked = true; +function rememberSelected(row, id) { + if($.inArray(id, rowList) > -1){ + row.style.background = "#ffa"; + row.cells[0].firstChild.checked = true; } } window.addEventListener("keydown", function(e){ var currentRowIndex; + var current_row; if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) { e.preventDefault(); } if (e.keyCode == 40) { if (e.shiftKey === true){ currentRowIndex = rowIndexList[rowIndexList.length-1]; if (currentRowIndex < 9){ - rowToAdd = currentRowIndex + 1; - if($.inArray(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr, 'Id').innerText, rowList) <= -1){ - rowIndexList.push(rowToAdd); - rowList.push(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr, 'Id').innerText); - oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr.style.background = "#ffa"; + row_index = currentRowIndex + 1; + current_row = oSettings.aoData[oSettings.aiDisplay[row_index]].nTr; + if($.inArray(current_row.row_id, rowList) <= -1){ + rowIndexList.push(row_index); + rowList.push(current_row.row_id); + current_row.style.background = "#ffa"; } } } else{ if (hoveredRow < 9){ if (hoveredRow != -1){ unhoverRow(oSettings.aoData[oSettings.aiDisplay[hoveredRow]].nTr); } hoveredRow++; hoverRow(oSettings.aoData[oSettings.aiDisplay[hoveredRow]].nTr); } } } else if (e.keyCode == 38) { if (e.shiftKey === true){ currentRowIndex = rowIndexList[rowIndexList.length-1]; if (currentRowIndex > 0){ rowToAdd = currentRowIndex - 1; - if($.inArray(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr, 'Id').innerText, rowList) <= -1){ + var current_row = oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr; + if($.inArray(current_row.row_id, rowList) <= -1){ rowIndexList.push(rowToAdd); - rowList.push(selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr, 'Id').innerText); - oSettings.aoData[oSettings.aiDisplay[rowToAdd]].nTr.style.background = "#ffa"; + rowList.push(current_row.row_id); + current_row.style.background = "#ffa"; } } } else{ if (hoveredRow > 0){ unhoverRow(oSettings.aoData[oSettings.aiDisplay[hoveredRow]].nTr); hoveredRow--; hoverRow(oSettings.aoData[oSettings.aiDisplay[hoveredRow]].nTr); } } } else if (e.keyCode == 37){ oTable.fnPageChange('previous'); } else if( e.keyCode == 39){ oTable.fnPageChange('next'); } else if (e.keyCode == 65 && e.ctrlKey === true){ selectAll(); removeSelection(); } else if (e.keyCode == 13 && hoveredRow != -1){ selectCellByTitle(oSettings.aoData[oSettings.aiDisplay[hoveredRow]].nTr, 'Details').click(); } else if(e.keyCode == 46){ if (rowList.length >= 1){ var rowList_out = JSON.stringify(rowList); deleteRecords(rowList_out); rowList = []; rowIndexList = []; } } }); function selectCellByTitle(row, title){ for(var i=0; i<oSettings.aoHeader[0].length; i++){ var trimmed_title = $.trim(oSettings.aoHeader[0][i].cell.innerText); if(trimmed_title === title){ - return row.cells[i]; + return $(row).children()[i - 1]; } } } +function getCellIndex(row, title){ + for(var i=0; i<oSettings.aoHeader[0].length; i++){ + var trimmed_title = $.trim(oSettings.aoHeader[0][i].cell.innerText); + if(trimmed_title === title){ + return i + } + } +} + function selectRow(row, e, oSettings) { selectedRow = row; + var widget_name; if( e.shiftKey === true ){ selectRange(row); } else{ - var widget_name = selectCellByTitle(row, 'Actions').innerText; - widget_name = widget_name.substring(0, widget_name.length-4); - - if($.inArray(selectCellByTitle(row, 'Id').innerText, rowList) <= -1){ - if (selectCellByTitle(row, 'Actions').innerText != 'N/A'){ - rowList.push(selectCellByTitle(row, 'Id').innerText); - rowIndexList.push(row._DT_RowIndex+oSettings._iDisplayStart); - selectedRow.style.background = "#ffa"; - + if(selectCellByTitle(row, 'Actions').childNodes[0].id === 'submitButtonMini'){ + widget_name = 'Approve Record'; + } + if($.inArray(row.row_id, rowList) <= -1){ + // Select row + rowList.push(row.row_id); + rowIndexList.push(row._DT_RowIndex+oSettings._iDisplayStart); + row.style.background = "#ffa"; + + if (selectCellByTitle(row, 'Actions').innerText != 'N/A'){ if(widget_name === 'Approve Record'){ - recordsToApprove.push(selectCellByTitle(row, 'Id').innerText); + recordsToApprove.push(row.row_id); + console.log(recordsToApprove); } - checkbox = selectCellByTitle(row, "").childNodes[1]; - console.log(checkbox); - checkbox.checked = true; - } - } + } + row.checkbox.checked = true; + } else{ - rowList.splice(rowList.indexOf(selectCellByTitle(row, 'Id').innerText), 1); + // De-Select + rowList.splice(rowList.indexOf(row.row_id), 1); rowIndexList.splice(rowIndexList.indexOf(row._DT_RowIndex+oSettings._iDisplayStart), 1); - selectedRow.style.background = "white"; + row.style.background = "white"; if(widget_name === 'Approve Record'){ - recordsToApprove.splice(recordsToApprove.indexOf(selectCellByTitle(row, 'Id').innerText), 1); + recordsToApprove.splice(recordsToApprove.indexOf(row.row_id), 1); } - checkbox = selectCellByTitle(row, "").childNodes[1]; - checkbox.checked = false; + row.checkbox.checked = false; } } checkRecordsToApprove(); - - console.log(rowList); - console.log(rowIndexList); - console.log(recordsToApprove); } function deselectAll(){ rowList = []; rowIndexList = []; oTable.fnDraw(false); window.getSelection().removeAllRanges(); } +function deselectAllFromPage(){ + var fromPos = oSettings._iDisplayStart; + var toPos = oSettings._iDisplayLength-1 + fromPos; + + for (i=fromPos; i<=toPos; i++){ + j = i % 10; + if($.inArray(i, rowIndexList) > -1){ + current_row = oSettings.aoData[oSettings.aiDisplay[j]].nTr; + selectRow(current_row, event, oSettings); + } + } +} + $(document).keyup(function(e){ if (e.keyCode == 27) { // esc deselectAll(); } }); //*********************************** // Tags functions //*********************************** $('.task-btn').on('click', function(){ if($.inArray($(this)[0].name, tagList) <= -1){ var widget_name = $(this)[0].name; - $('#tag-area').append('<div class="alert alert-info tag-alert col-md-1">'+widget_name+'<a id="'+widget_name+'class="close-btn" data-dismiss="alert" name='+widget_name+' onclick="closeTag(this.parentElement)">×</a></div>'); + $('#tagsinput').tagsinput('add', $(this)[0].name); tagList.push($(this)[0].name); - oTable.fnFilter($(this)[0].name); - + requestNewObjects(); } else{ - closeTag($('#'+widget_name)); + closeTag(widget_name); oTable.fnFilter( '^$', 4, true, false ); - $('#refresh_button').click(); + oTable.fnDraw(false); } - // requestNewObjects(); }); $('.version-selection').on('click', function(){ - console.log($(this)[0].name); - console.log(tagList); - if($.inArray($(this)[0].name, tagList) <= -1){ - console.log("TAG NOT IN TAGLIST"); - $('#tag-area').append('<div id="tag-version-'+$(this)[0].name+'" name="'+$(this)[0].name+'" class="alert alert-info tag-alert col-md-1">'+$(this)[0].name+'<a class="close-btn pull-right" data-dismiss="alert" onclick="closeTag(this.parentElement)">×</a></div>'); + $('#tagsinput').tagsinput('add', $(this)[0].name); tagList.push($(this)[0].name); - } - else{ - closeTag($('#tag-version-'+$(this)[0].name)[0]); - } + requestNewObjects(); + } +}); + +$("#tagsinput").on('itemRemoved', function(event) { + tagList.splice(tagList.indexOf(event.item), 1); + console.log('item removed : '+event.item); + oTable.fnFilter(''); requestNewObjects(); }); -function closeTag(obj){ - // console.log(tagList); - // console.log(obj); - // console.log(obj.name); - var tag_name = obj.innerText.substr(0,obj.innerText.length-1); +$("#tagsinput").on('itemAdded', function(event){ + if(event.item != 'Halted' && event.item != 'Final' && event.item != 'Running'){ + oTable.fnFilter(event.item); + } +}); + +function closeTag(tag_name){ console.log(tag_name); - tagList.splice(tagList.indexOf(obj.name), 1); - - obj.remove(); + tagList.splice(tagList.indexOf(tag_name), 1); + $('#tagsinput').tagsinput('remove', tag_name); + console.log($("#tagsinput").tagsinput('items')); requestNewObjects(); }; //*********************************** //Utility functions //*********************************** +function initialize_versions(version_showing){ + if(version_showing){ + for(var i=0; i<version_showing.length; i++){ + if(version_showing[i] == 1){ + if ($.inArray('Final', tagList) <= -1){ + tagList.push('Final'); + } + $('#version-final').click(); + } + else if(version_showing[i] == 2){ + // tagList.push('Halted'); + $('#version-halted').click(); + } + else if(version_showing[i] == 3){ + if ($.inArray('Halted', tagList) <= -1){ + tagList.push('Running'); + } + $('#version-running').click(); + } + } + } +} + function fnGetSelected( oTableLocal ){ var aReturn = []; var aTrs = oTableLocal.fnGetNodes(); for ( var i=0 ; i<aTrs.length ; i++ ){ if ($(aTrs[i]).hasClass('row_selected')){ aReturn.push( aTrs[i] ); } } return aReturn; } function requestNewObjects(){ var version_showing = new Object; // var widget_showing = new Object; version_showing['final'] = ($.inArray('Final', tagList) <= -1) ? false : true; version_showing['halted'] = ($.inArray('Halted', tagList) <= -1) ? false : true; version_showing['running'] = ($.inArray('Running', tagList) <= -1) ? false : true; $.ajax({ type : "POST", url : url.load_table, data: JSON.stringify(version_showing), contentType: 'application/json;charset=UTF-8', traditional: true, success: function(result) { - $('#refresh_button').click(); + oTable.fnDraw(false); } }); } function isInt(n) { return typeof n === 'number' && n % 1 === 0; } function emptyLists(){ rowList = []; rowIndexList = []; } +$.fn.exists = function () { + return this.length !== 0; +} + function bootstrap_alert(message) { $('#alert-message').html('<span class="alert"><a class="close" data-dismiss="alert"> ×</a><span>'+message+'</span></span>'); } //*********************************** diff --git a/invenio/modules/workflows/static/js/workflows/index.js b/invenio/modules/workflows/static/js/workflows/index.js index d3aa1479b..3640c9275 100644 --- a/invenio/modules/workflows/static/js/workflows/index.js +++ b/invenio/modules/workflows/static/js/workflows/index.js @@ -1,86 +1,85 @@ /* * This file is part of Invenio. * Copyright (C) 2013 CERN. * * Invenio is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Invenio is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Invenio; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ function get_redis_data(url){ a = ""; tags = $("span.sort_tag"); for (i=0; i<tags.length; i++){ a += $(tags[i]).attr('name')+" "; } jQuery.ajax({ url: url, data: {'key': a}, success: function(json){ - alert(json)} }) } function init_index(url_redis_get, url_entry, url_workflow) { $("tbody > tr.workflow").bind('click', function(){ hp_id = $(this).attr('name'); jQuery.ajax({ url: url_workflow, data: {'id_workflow': hp_id}, success: function(json){ $("#myModal").html(json); $('#myModal').modal('show');} }) }); $("tbody > tr.object").bind('click', function(){ hp_id = $(this).attr('name'); jQuery.ajax({ url: url_entry, data: {'id_entry': hp_id}, success: function(json){ $("#myModal").html(json); $('#myModal').modal('show');} }) }); $(".entry_message_button").bind('click', function(){ hp_id = $(this).attr('name'); TINY.box.show({url:"/match_details/1" + hp_id,width:"800",height:"600", animate:false}); }); jQuery.ajax({ url: url_redis_get, data: {'key': $("form select#search_key_1").val()}, success: function(json){ $("form select#search_key_2").html(json); $("form select#search_key_2").removeAttr('disabled'); }}); $("form select#search_key_1").bind('change', function(){ key = $(this).val(); jQuery.ajax({ url: url_redis_get, data: {'key': key}, success: function(json){ $("form select#search_key_2").html(json); $("form select#search_key_2").removeAttr('disabled'); }}); }); $("form span#search_button").bind('click', function(){ a = $("div#search_tags").html() a += "<span class='label sort_tag' name='"+$("form select#search_key_1").val()+":"+$("form select#search_key_2").val()+"'>"+$("form select#search_key_1").val()+": "+$("form select#search_key_2").val()+" <i class='icon-remove remove_sort_tag'></i></span> " $("div#search_tags").html(a) get_redis_data(url_redis_get); }); } diff --git a/invenio/modules/workflows/static/js/workflows/widgets/approval.js b/invenio/modules/workflows/static/js/workflows/widgets/approval.js index 9d29340fd..ad1e1c44e 100644 --- a/invenio/modules/workflows/static/js/workflows/widgets/approval.js +++ b/invenio/modules/workflows/static/js/workflows/widgets/approval.js @@ -1,191 +1,211 @@ // -*- coding: utf-8 -*- // This file is part of Invenio. // Copyright (C) 2013 CERN. // // Invenio is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // Invenio is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Invenio; if not, write to the Free Software Foundation, Inc., // 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. - var bwoid; var datapreview = "hd"; var number_of_objs = $(".theform").length; var current_number = number_of_objs-1; url = new Object(); function init_urls_approval(url_){ url = url_; } function checkRecordsToApprove(){ if(recordsToApprove.length > 1){ hideApproveAll(); approveAll(); } else{ hideApproveAll(); } } function disapproveRecords(){ console.log("deleting"); deleteRecords(recordsToApprove); recordsToApprove = []; // TODO: // the bug here will occur when there are records with other widgets // than approval. emptyLists(); checkRecordsToApprove(); } function hideApproveAll(){ $('#multi-approval').empty(); } function approveAll() { var rejectBtn = '<button type="button" class="btn btn-danger">'+ '<a id="reject-multi" href="#confirmationModal" class="mini-approval-btn" data-toggle="modal">'+ 'Reject</a></button>'; var acceptBtn = '<button type="button" class="btn btn-success">'+ '<a id="accept-multi" href="javascript:void(0)" class="mini-approval-btn">'+ 'Accept</a></button>'; - $('#multi-approval').append(rejectBtn, acceptBtn); - $('#accept-multi').click( function(){ + + if(!$('#batch-btn').exists()){ + var accept_link = "<a id='drop-down-accept' class='drop-down-btns btn' href='#'>Accept All</a>"; + var reject_link = "<a id='drop-down-reject' class='drop-down-btns btn' href='#'>Reject All</a>"; + + var batch_btn = '<li class="dropdown">'+ + '<a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>'+ + '<ul class="dropdown-menu">'+ + '<li>'+accept_link+'</li>'+ + '<li>'+reject_link+'</li>'+ + '<li class="divider"></li>'+ + '<li><a href="#">Go to Widget</a></li>'+ + '</ul>'+ + '</li>'; + + $('#navbar-right').append(batch_btn); + $('.dropdown-toggle').dropdown(); + } + + $('#drop-down-accept').on('click', function(){ for(i=0; i<recordsToApprove.length; i++){ + console.log(recordsToApprove[i]); jQuery.ajax({ type: "POST", url: url.resolve_widget, - data: {'bwobject_id': recordsToApprove[i], + data: {'objectid': recordsToApprove[i], 'widget': 'approval_widget', 'decision': 'Accept'}, success: function(json){ recordsToApprove = []; $('#refresh_button').click(); checkRecordsToApprove(); } }); } }); -}; -function mini_approval(decision, event){ - var bwobject_id = event.currentTarget.parentElement.parentElement.cells[1].innerText; + $('#drop-down-reject').on('click', function(){ + for(i=0; i<recordsToApprove.length; i++){ + console.log(recordsToApprove[i]); + jQuery.ajax({ + type: "POST", + url: url.resolve_widget, + data: {'objectid': recordsToApprove[i], + 'widget': 'approval_widget', + 'decision': 'Reject'}, + success: function(json){ + recordsToApprove = []; + $('#refresh_button').click(); + checkRecordsToApprove(); + } + }); + } + }); +} +function mini_approval(decision, event, objectid){ jQuery.ajax({ type: "POST", url: url.resolve_widget, - data: {'bwobject_id': bwobject_id, + data: {'objectid': objectid, 'widget': "approval_widget", 'decision': decision}, success: function(json){ deselectAll(); recordsToApprove = []; $('#refresh_button').click(); checkRecordsToApprove(); } }); oTable.fnDraw(false); }; function deleteRecords(bwolist){ for(i=0; i<recordsToApprove.length; i++){ console.log(bwolist[i]); jQuery.ajax({ url: url.delete_single, - data: {'bwobject_id': bwolist[i]}, + data: {'objectid': bwolist[i]}, success: function(){ $('#refresh_button').click(); } }); } }; $(document).ready(function(){ $(".message").hide(); + $("#batch-btn").popover(); $('.theform #submitButton').click( function(event) { event.preventDefault(); - var form_id = $(this)[0].form.parentElement.previousElementSibling.id; - id_number = form_id.substring(form_id.indexOf("d")+1); - btn_div_id = "decision-btns"+id_number; - hr_id = "hr"+id_number; + var form_name = $(this)[0].form.name; + var bwo_id = form_name.substring(form_name.indexOf('bwobject_id')+12); + var form_id = $(this)[0].form.id.substring(4); + + btn_div_id = "decision-btns"+form_id; + hr_id = "hr"+form_id; formdata = $(this)[0].value; formurl = event.currentTarget.parentElement.name; + console.log(formurl); $.ajax({ type: "POST", url: formurl, data: {'decision': formdata}, success: function(data){ $("#"+form_id).fadeOut(400); $("#"+btn_div_id).fadeOut(400); $("#"+hr_id).fadeOut(400); current_number--; } }); if (current_number === 0){ $("#goodbye-msg").text("All Done!"); } }); window.setbwoid = function(id){ bwoid = id; console.log(bwoid); data_preview(url_preview, bwoid, datapreview); }; window.setDataPreview = function(dp, id){ bwoid = id; datapreview = dp; - console.log(url_preview); - data_preview(url_preview, bwoid, datapreview); + console.log(url.preview); + data_preview(url.preview, bwoid, datapreview); } $('#submitButtonMini').click( function (event){ console.log(event); }); $('body').append( '<div id="confirmationModal" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">'+ '<div class="modal-header">'+ '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>'+ '<h3 id="myModalLabel">Please Confirm</h3>'+ '</div>'+ '<div class="modal-body">'+ '<p>Are you sure you want to delete the selected records?</p>'+ '</div>'+ '<div class="modal-footer">'+ '<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>'+ '<a class="btn btn-danger" href="#" data-dismiss="modal" onclick="disapproveRecords()">Delete Records</a>'+ '</div>'+ '</div>'); - // $.ajax({ - // url: "hp_maintable.html", - // success: function (data) { $('body').append( - // '<div id="confirmationModal" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">'+ - // '<div class="modal-header">'+ - // '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>'+ - // '<h3 id="myModalLabel">Please Confirm</h3>'+ - // '</div>'+ - // '<div class="modal-body">'+ - // '<p>Are you sure you want to delete the selected records?</p>'+ - // '</div>'+ - // '<div class="modal-footer">'+ - // '<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>'+ - // '<a class="btn btn-danger" href="#" data-dismiss="modal" onclick="disapproveRecords()">Delete Records</a>'+ - // '</div>'+ - // '</div>'); }, - // dataType: 'html' - // }); -}); \ No newline at end of file +}); diff --git a/invenio/modules/workflows/tasks/marcxml_tasks.py b/invenio/modules/workflows/tasks/marcxml_tasks.py index bc4c510c0..8a035e8e3 100644 --- a/invenio/modules/workflows/tasks/marcxml_tasks.py +++ b/invenio/modules/workflows/tasks/marcxml_tasks.py @@ -1,941 +1,937 @@ ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. import os import random import time import glob import re import traceback from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from invenio.legacy.bibupload.engine import (find_record_from_recid, find_record_from_sysno, find_records_from_extoaiid, find_record_from_oaiid, find_record_from_doi ) from invenio.legacy.oaiharvest.dblayer import update_lastrun, create_oaiharvest_log_str from invenio.base.config import (CFG_TMPSHAREDDIR, CFG_TMPDIR, CFG_INSPIRE_SITE) from invenio.legacy.oaiharvest.utils import (record_extraction_from_file, collect_identifiers, harvest_step, translate_fieldvalues_from_latex, find_matching_files, ) from invenio.legacy.bibsched.bibtask import (task_sleep_now_if_required, task_low_level_submission ) from invenio.modules.oaiharvester.models import OaiHARVEST -from invenio.modules.records.api import Record +from invenio.modules.records.api import Record, create_record from invenio.modules.workflows.errors import WorkflowError from invenio.legacy.refextract.api import extract_references_from_file_xml from invenio.legacy.bibrecord import (create_records, record_xml_output ) from invenio.utils.plotextractor.output_utils import (create_MARC, create_contextfiles, prepare_image_data, remove_dups ) from invenio.utils.plotextractor.getter import (harvest_single, make_single_directory ) from invenio.utils.plotextractor.cli import (get_defaults, extract_captions, extract_context ) from invenio.utils.shell import (run_shell_command, Timeout ) import invenio.legacy.template from invenio.utils.plotextractor.converter import (untar, convert_images ) oaiharvest_templates = invenio.legacy.template.load('oaiharvest') REGEXP_REFS = re.compile("<record.*?>.*?<controlfield .*?>.*?</controlfield>(.*?)</record>", re.DOTALL) REGEXP_AUTHLIST = re.compile("<collaborationauthorlist.*?</collaborationauthorlist>", re.DOTALL) def add_metadata_to_extra_data(obj, eng): """ Creates bibrecord from object data and populates extra_data with metadata @param obj: @param eng: """ obj.extra_data["_last_task_name"] = "add_metadata_to_extra_data" - from invenio.legacy.bibrecord import create_record, record_get_field_value + from invenio.legacy.bibrecord import create_record as old_create_record, record_get_field_value - record = create_record(obj.data) + record = old_create_record(obj.data) obj.extra_data['redis_search']['category'] = \ record_get_field_value(record[0], '037', code='c') obj.extra_data['redis_search']['title'] = \ record_get_field_value(record[0], '245', code='a') obj.extra_data['redis_search']['source'] = \ record_get_field_value(record[0], '035', code='9') add_metadata_to_extra_data.__title__ = "Metadata Extraction" add_metadata_to_extra_data.__description__ = "Populates object's extra_data with metadata" def approve_record(obj, eng): """ Will add the approval widget to the record """ try: eng.halt(widget="approval_widget", msg='Record needs approval') except KeyError: # Log the error obj.extra_data["_error_msg"] = 'Could not assign widget' approve_record.__title__ = "Record Approval" approve_record.__description__ = "This task assigns the approval widget to a record." def filtering_oai_pmh_identifier(obj, eng): if not "_function_reserved_filtering_oai_pmh_identifier" in eng.extra_data: eng.extra_data["_function_reserved_filtering_oai_pmh_identifier"] = {} if not "identifiers" in eng.extra_data["_function_reserved_filtering_oai_pmh_identifier"]: eng.extra_data["_function_reserved_filtering_oai_pmh_identifier"]["identifiers"] = [] try: if not isinstance(obj.data, list): obj_data_list = [obj.data] else: obj_data_list = obj.data for record in obj_data_list: substring = record[record.index("<identifier>") + 12:record.index("</identifier>")] if substring in eng.extra_data["_function_reserved_filtering_oai_pmh_identifier"]["identifiers"]: return False else: eng.extra_data["_function_reserved_filtering_oai_pmh_identifier"]["identifiers"].append(substring) return True except TypeError: eng.log.error("object data type invalid. Ignoring this step!") return True def inspire_filter_custom(fields, custom_accepted=(), custom_refused=(), custom_widgeted=(), widget=None): def _inspire_filter_custom(obj, eng): custom_to_process_current = [] custom_to_process_next = [] action_to_take = [0, 0, 0] fields_to_process = fields if not isinstance(fields_to_process, list): fields_to_process = [fields_to_process] for field in fields_to_process: if len(custom_to_process_current) == 0: custom_to_process_current.append(obj.data[field]) else: while len(custom_to_process_current) > 0: one_custom = custom_to_process_current.pop() if isinstance(one_custom, list): for i in one_custom: custom_to_process_current.append(i) else: try: custom_to_process_next.append(one_custom[field]) except KeyError: eng.log.error("no %s in %s", field, one_custom) custom_to_process_current = custom_to_process_next[:] if not custom_to_process_next: eng.log.error("%s not found in the record. Human intervention needed", fields_to_process) eng.halt(str(fields_to_process) + " not found in the record. Human intervention needed", widget=widget) for i in custom_widgeted: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in custom_to_process_next: if i.match(y): action_to_take[0] += 1 for i in custom_accepted: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in custom_to_process_next: if i.match(y): action_to_take[1] += 1 for i in custom_refused: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in custom_to_process_next: if i.match(y): action_to_take[2] += 1 sum_action = action_to_take[0] + action_to_take[1] + action_to_take[2] if sum_action == 0: #We allow the * option which means at final case if '*' in custom_widgeted: return None elif '*' in custom_refused: eng.stopProcessing() elif '*' in custom_accepted: return None else: # We don't know what we should do, in doubt query human... they are nice! msg = ("Category out of task definition. " "Human intervention needed") eng.halt(msg, widget=widget) else: if sum_action == action_to_take[0]: eng.halt("Category filtering needs human intervention", widget=widget) elif sum_action == action_to_take[1]: return None elif sum_action == action_to_take[2]: eng.stopProcessing() else: eng.halt("Category filtering needs human intervention, rules are incoherent !!!", widget=widget) return _inspire_filter_custom def inspire_filter_category(category_accepted_param=(), category_refused_param=(), category_widgeted_param=(), widget_param=None): def _inspire_filter_category(obj, eng): try: category_accepted = obj.extra_data["_repository"]["arguments"]["filtering"]['category_accepted'] except KeyError: category_accepted = category_accepted_param try: category_refused = obj.extra_data["_repository"]["arguments"]["filtering"]['category_refused'] except KeyError: category_refused = category_refused_param try: category_widgeted = obj.extra_data["_repository"]["arguments"]["filtering"]['category_widgeted'] except KeyError: category_widgeted = category_widgeted_param try: widget = obj.extra_data["_repository"]["arguments"]["filtering"]['widget'] except KeyError: widget = widget_param category_to_process = [] action_to_take = [0, 0, 0] try: category = obj.data["report_number"] if isinstance(category, list): for i in category: category_to_process.append(i["arxiv_category"]) else: category_to_process.append(category["arxiv_category"]) obj.add_task_result("Category filter", category_to_process) except KeyError: msg = "Category not found in the record. Human intervention needed" eng.log.error(msg) eng.halt(msg, widget=widget) for i in category_widgeted: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in category_to_process: if i.match(y): action_to_take[0] += 1 for i in category_accepted: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in category_to_process: if i.match(y): action_to_take[1] += 1 for i in category_refused: if i != '*': i = re.compile('^' + re.escape(i) + '.*') for y in category_to_process: if i.match(y): action_to_take[2] += 1 sum_action = action_to_take[0] + action_to_take[1] + action_to_take[2] if sum_action == 0: #We allow the * option which means at final case if '*' in category_accepted: return None elif '*' in category_refused: eng.stopProcessing() else: # We don't know what we should do, in doubt query human... they are nice! msg = ("Category out of task definition. " "Human intervention needed") eng.halt(msg, widget=widget) else: if sum_action == action_to_take[0]: eng.halt("Category filtering needs human intervention", widget=widget) elif sum_action == action_to_take[1]: return None elif sum_action == action_to_take[2]: eng.stopProcessing() else: eng.halt("Category filtering needs human intervention, rules are incoherent !!!", widget=widget) return _inspire_filter_category def convert_record_to_bibfield(obj, eng): """ Convert a record in data log.errorinto a 'dictionary' thanks to BibField """ + obj.extra_data["last_task_name"] = "last task name: convert_record_to_bibfield" obj.data = create_record(obj.data, master_format="marc").dumps() eng.log.info("Field conversion succeeded") def init_harvesting(obj, eng): """ This function gets all the option linked to the task and stores them into the object to be used later. """ try: obj.extra_data["options"] = eng.extra_data["options"] except KeyError: eng.log.error("Non Critical Error: No options", "No options for this task have been found. It is possible" "that the following task could failed or work not as expected") obj.extra_data["options"] = {} eng.log.info("end of init_harvesting") def get_repositories_list(repositories=()): """ Here we are retrieving the oaiharvest configuration for the task. It will allows in the future to do all the correct operations. """ def _get_repositories_list(obj, eng): repositories_to_harvest = repositories reposlist_temp = [] if obj.extra_data["options"]["repository"]: repositories_to_harvest = obj.extra_data["options"]["repository"] if repositories_to_harvest: for reposname in repositories_to_harvest: try: reposlist_temp.append(OaiHARVEST.get(OaiHARVEST.name == reposname).one()) except (MultipleResultsFound, NoResultFound): eng.log.error("CRITICAL: repository %s doesn't exit into our database", reposname) else: reposlist_temp = OaiHARVEST.get(OaiHARVEST.name != "").all() true_repo_list = [] for repo in reposlist_temp: true_repo_list.append(repo.to_dict()) if true_repo_list: return true_repo_list else: eng.halt("No Repository named %s. Impossible to harvest non-existing things." % repositories_to_harvest) return _get_repositories_list def harvest_records(obj, eng): """ Run the harvesting task. The row argument is the oaiharvest task queue row, containing if, arguments, etc. Return 1 in case of success and 0 in case of failure. """ harvested_identifier_list = [] harvestpath = "%s_%d_%s_" % ("%s/oaiharvest_%s" % (CFG_TMPSHAREDDIR, eng.uuid), 1, time.strftime("%Y%m%d%H%M%S")) # ## go ahead: check if user requested from-until harvesting try: if "dates" not in obj.extra_data["options"]: obj.extra_data["options"]["dates"] = [] if "identifiers" not in obj.extra_data["options"]: obj.extra_data["options"]["identifiers"] = [] except TypeError: obj.extra_data["options"] = {"dates": [], "identifiers": []} task_sleep_now_if_required() arguments = obj.extra_data["_repository"]["arguments"] if arguments: eng.log.info("running with post-processes: %r" % (arguments,)) else: eng.log.error("No arguments found... It can be causing major error after this point.") # Harvest phase try: harvested_files_list = harvest_step(obj, harvestpath) except Exception as e: eng.log.error("Error while harvesting %s. Skipping." % (obj.data,)) raise WorkflowError("Error while harvesting %r. Skipping : %s." % (obj.data, repr(e)), id_workflow=eng.uuid, id_object=obj.id) if len(harvested_files_list) == 0: eng.log.info("No records harvested for %s" % (obj.data["name"],)) # Retrieve all OAI IDs and set active list harvested_identifier_list.append(collect_identifiers(harvested_files_list)) if len(harvested_files_list) != len(harvested_identifier_list[0]): # Harvested files and its identifiers are 'out of sync', abort harvest raise WorkflowError("Harvested files miss identifiers for %s" % (arguments,), id_workflow=eng.uuid, id_object=obj.id) obj.extra_data['harvested_files_list'] = harvested_files_list eng.log.info("%d files harvested and processed \n End harvest records task" % (len(harvested_files_list),)) harvest_records.__id__ = "h" def get_records_from_file(path=None): def _get_records_from_file(obj, eng): if not "_LoopData" in eng.extra_data: eng.extra_data["_LoopData"] = {} if "get_records_from_file" not in eng.extra_data["_LoopData"]: eng.extra_data["_LoopData"]["get_records_from_file"] = {} if path: eng.extra_data["_LoopData"]["get_records_from_file"].update({"data": record_extraction_from_file(path)}) else: eng.extra_data["_LoopData"]["get_records_from_file"].update( {"data": record_extraction_from_file(obj.data)}) eng.extra_data["_LoopData"]["get_records_from_file"]["path"] = obj.data elif os.path.isfile(obj.data) and obj.data != eng.extra_data["_LoopData"]["get_records_from_file"]["path"]: eng.extra_data["_LoopData"]["get_records_from_file"].update({"data": record_extraction_from_file(obj.data)}) return eng.extra_data["_LoopData"]["get_records_from_file"]["data"] return _get_records_from_file def get_eng_uuid_harvested(obj, eng): """ Simple function which allows to retrieve the uuid of the eng in the workflow for printing by example """ eng.log.info("last task name: get_eng_uuid_harvested") return "*" + str(eng.uuid) + "*.harvested" def get_files_list(path, parameter): def _get_files_list(obj, eng): if callable(parameter): unknown = parameter while callable(unknown): unknown = unknown(obj, eng) else: unknown = parameter result = glob.glob1(path, unknown) for i in range(0, len(result)): result[i] = path + os.sep + result[i] return result return _get_files_list def get_obj_extra_data_key(name): def _get_obj_extra_data_key(obj, eng): return obj.extra_data[name] return _get_obj_extra_data_key def get_eng_extra_data_key(name): def _get_eng_extra_data_key(obj, eng): return eng.extra_data[name] return _get_eng_extra_data_key def convert_record(stylesheet="oaidc2marcxml.xsl"): def _convert_record(obj, eng): """ Will convert the object data, if XML, using the given stylesheet """ from invenio.legacy.bibconvert.xslt_engine import convert eng.log.info("Starting conversion using %s stylesheet" % (stylesheet,)) try: obj.data = convert(obj.data, stylesheet) except Exception as e: msg = "Could not convert record: %s\n%s" % \ (str(e), traceback.format_exc()) obj.extra_data["_error_msg"] = msg raise WorkflowError("Error: %s" % (msg,), id_workflow=eng.uuid, id_object=obj.id) return _convert_record def convert_record_with_repository(stylesheet="oaidc2marcxml.xsl"): def _convert_record(obj, eng): """ Will convert the object data, if XML, using the stylesheet in the OAIrepository stored in the object extra_data. """ eng.log.info("my type: %s" % (obj.data_type,)) try: if not obj.extra_data["_repository"]["arguments"]['c_stylesheet']: stylesheet_to_use = stylesheet else: stylesheet_to_use = obj.extra_data["_repository"]["arguments"]['c_stylesheet'] except KeyError: eng.log.error("WARNING: HASARDOUS BEHAVIOUR EXPECTED, " "You didn't specified style_sheet in argument for conversion," "try to recover by using the default one!") stylesheet_to_use = stylesheet convert_record(stylesheet_to_use)(obj, eng) return _convert_record def update_last_update(repository_list): def _update_last_update(obj, eng): if "_should_last_run_be_update" in obj.extra_data: if obj.extra_data["_should_last_run_be_update"]: repository_list_to_process = repository_list if not isinstance(repository_list_to_process, list): if callable(repository_list_to_process): while callable(repository_list_to_process): repository_list_to_process = repository_list_to_process(obj, eng) else: repository_list_to_process = [repository_list_to_process] for repository in repository_list_to_process: update_lastrun(repository["id"]) return _update_last_update def fulltext_download(obj, eng): """ Performs the fulltext download step. Only for arXiv """ if "result" not in obj.extra_data: obj.extra_data["_result"] = {} task_sleep_now_if_required() if "pdf" not in obj.extra_data["_result"]: extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) tarball, pdf = harvest_single(obj.data["system_number_external"]["value"], extract_path, ["pdf"]) arguments = obj.extra_data["_repository"]["arguments"] try: if not arguments['t_doctype'] == '': doctype = arguments['t_doctype'] else: doctype = 'arXiv' except KeyError: eng.log.error("WARNING: HASARDOUS BEHAVIOUR EXPECTED, " "You didn't specified t_doctype in argument for fulltext_download," "try to recover by using the default one!") doctype = 'arXiv' if pdf: obj.extra_data["_result"]["pdf"] = pdf fulltext_xml = (" <datafield tag=\"FFT\" ind1=\" \" ind2=\" \">\n" " <subfield code=\"a\">%(url)s</subfield>\n" " <subfield code=\"t\">%(doctype)s</subfield>\n" " </datafield>" ) % {'url': obj.extra_data["_result"]["pdf"], 'doctype': doctype} updated_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<collection>\n<record>\n' + fulltext_xml + \ '</record>\n</collection>' - from invenio.modules.records.api import create_record - new_dict_representation = create_record(updated_xml).dumps() + new_dict_representation = create_record(updated_xml, master_format="marc").dumps() try: obj.data['fft'].append(new_dict_representation["fft"]) except (KeyError, TypeError): obj.data['fft'] = [new_dict_representation['fft']] obj.add_task_result("filesfft", new_dict_representation["fft"]) else: eng.log.info("There was already a pdf register for this record," "perhaps a duplicate task in you workflow.") def quick_match_record(obj, eng): """ Retrieve the record Id from a record by using tag 001 or SYSNO or OAI ID or DOI tag. opt_mod is the desired mode. 001 fields even in the insert mode """ function_dictionnary = {'recid': find_record_from_recid, 'system_number': find_record_from_sysno, 'oaiid': find_record_from_oaiid, 'system_number_external': find_records_from_extoaiid, 'doi': find_record_from_doi} my_json_reader = Record(obj.data) try: identifiers = my_json_reader.get('_persistent_identifier') if not identifiers: return False else: obj.persistent_ids = identifiers except KeyError: identifiers = {} if not "recid" in identifiers: for identifier in identifiers: recid = function_dictionnary[identifier](identifiers[identifier]["value"]) if recid: obj.data['recid']['value'] = recid obj.persistent_ids["recid"] = recid return True return False else: return True def upload_record(mode="ir"): def _upload_record(obj, eng): from invenio.legacy.bibsched.bibtask import task_low_level_submission eng.log_info("Saving data to temporary file for upload") filename = obj.save_to_file() params = ["-%s" % (mode,), filename] task_id = task_low_level_submission("bibupload", "bibworkflow", *tuple(params)) eng.log_info("Submitted task #%s" % (task_id,)) _upload_record.__title__ = "Upload Record" _upload_record.__description__ = "Uploads the record using BibUpload" return _upload_record upload_record.__id__ = "u" def plot_extract(plotextractor_types): def _plot_extract(obj, eng): """ Performs the plotextraction step. """ # Download tarball for each harvested/converted record, then run plotextrator. # Update converted xml files with generated xml or add it for upload task_sleep_now_if_required() if "_result" not in obj.extra_data: obj.extra_data["_result"] = {} if not 'p_extraction-source' in obj.extra_data["_repository"]["arguments"]: p_extraction_source = plotextractor_types else: p_extraction_source = obj.extra_data["_repository"]["arguments"]['p_extraction-source'] if not isinstance(p_extraction_source, list): p_extraction_source = [p_extraction_source] if 'latex' in p_extraction_source: # Run LaTeX plotextractor if "tarball" not in obj.extra_data["_result"]: # turn oaiharvest_23_1_20110214161632_converted -> oaiharvest_23_1_material # to let harvested material in same folder structure extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) tarball, pdf = harvest_single(obj.data["system_number_external"]["value"], extract_path, ["tarball"]) tarball = str(tarball) if tarball is None: raise WorkflowError(str("Error harvesting tarball from id: %s %s" % (obj.data["system_number_external"]["value"], extract_path)), eng.uuid, id_object=obj.id) obj.extra_data["_result"]["tarball"] = tarball else: tarball = obj.extra_data["_result"]["tarball"] sub_dir, refno = get_defaults(tarball, CFG_TMPDIR, "") tex_files = None image_list = None try: extracted_files_list, image_list, tex_files = untar(tarball, sub_dir) except Timeout: eng.log.error('Timeout during tarball extraction on %s' % (tarball,)) converted_image_list = convert_images(image_list) eng.log.info('converted %d of %d images found for %s' % (len(converted_image_list), len(image_list), os.path.basename(tarball))) extracted_image_data = [] if tex_files == [] or tex_files is None: eng.log.error('%s is not a tarball' % (os.path.split(tarball)[-1],)) run_shell_command('rm -r %s', (sub_dir,)) else: for tex_file in tex_files: # Extract images, captions and labels partly_extracted_image_data = extract_captions(tex_file, sub_dir, converted_image_list) if partly_extracted_image_data: # Add proper filepaths and do various cleaning cleaned_image_data = prepare_image_data(partly_extracted_image_data, tex_file, converted_image_list) # Using prev. extracted info, get contexts for each image found extracted_image_data.extend((extract_context(tex_file, cleaned_image_data))) if extracted_image_data: extracted_image_data = remove_dups(extracted_image_data) create_contextfiles(extracted_image_data) marc_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<collection>\n' marc_xml += create_MARC(extracted_image_data, tarball, None) marc_xml += "\n</collection>" if marc_xml: - from invenio.modules.records.api import create_record # We store the path to the directory the tarball contents live # Read and grab MARCXML from plotextractor run - new_dict_representation = create_record(marc_xml).dumps() + new_dict_representation = create_record(marc_xml, master_format="marc").dumps() try: obj.data['fft'].append(new_dict_representation["fft"]) except KeyError: obj.data['fft'] = [new_dict_representation['fft']] obj.add_task_result("filesfft", new_dict_representation["fft"]) obj.add_task_result("number_picture_converted", len(converted_image_list)) obj.add_task_result("number_of_picture_total", len(image_list)) return _plot_extract def refextract(obj, eng): """ Performs the reference extraction step. """ task_sleep_now_if_required() if "_result" not in obj.extra_data: obj.extra_data["_result"] = {} if "pdf" not in obj.extra_data["_result"]: extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) tarball, pdf = harvest_single(obj.data["system_number_external"]["value"], extract_path, ["pdf"]) if pdf is not None: obj.extra_data["_result"]["pdf"] = pdf elif not os.path.isfile(obj.extra_data["_result"]["pdf"]): extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) tarball, pdf = harvest_single(obj.data["system_number_external"]["value"], extract_path, ["pdf"]) if pdf is not None: obj.extra_data["_result"]["pdf"] = pdf if os.path.isfile(obj.extra_data["_result"]["pdf"]): cmd_stdout = extract_references_from_file_xml(obj.extra_data["_result"]["pdf"]) references_xml = REGEXP_REFS.search(cmd_stdout) if references_xml: updated_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<collection>\n<record>' + references_xml.group(1) + \ "</record>\n</collection>" - from invenio.modules.records.api import create_record - - new_dict_representation = create_record(updated_xml).dumps() + new_dict_representation = create_record(updated_xml, master_format="marc").dumps() try: obj.data['reference'].append(new_dict_representation["reference"]) except KeyError: if 'reference' in new_dict_representation: obj.data['reference'] = [new_dict_representation['reference']] obj.add_task_result("reference", new_dict_representation['reference']) else: obj.log.error("Not able to download and process the PDF ") def author_list(obj, eng): """ Performs the special authorlist extraction step (Mostly INSPIRE/CERN related). """ identifiers = obj.data["system_number_external"]["value"] task_sleep_now_if_required() if "_result" not in obj.extra_data: obj.extra_data["_result"] = {} if "tarball" not in obj.extra_data["_result"]: extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) tarball, pdf = harvest_single(obj.data["system_number_external"]["value"], extract_path, ["tarball"]) tarball = str(tarball) if tarball is None: raise WorkflowError(str("Error harvesting tarball from id: %s %s" % (identifiers, extract_path)), eng.uuid, id_object=obj.id) obj.extra_data["_result"]["tarball"] = tarball sub_dir, dummy = get_defaults(obj.extra_data["_result"]["tarball"], CFG_TMPDIR, "") try: untar(obj.extra_data["_result"]["tarball"], sub_dir) except Timeout: eng.log.error('Timeout during tarball extraction on %s' % (obj.extra_data["_result"]["tarball"])) xml_files_list = find_matching_files(sub_dir, ["xml"]) authors = "" for xml_file in xml_files_list: xml_file_fd = open(xml_file, "r") xml_content = xml_file_fd.read() xml_file_fd.close() match = REGEXP_AUTHLIST.findall(xml_content) if not match == []: authors += match[0] # Generate file to store conversion results if authors is not '': from invenio.legacy.bibconvert.xslt_engine import convert authors = convert(authors, "authorlist2marcxml.xsl") authorlist_record = create_records(authors) if len(authorlist_record) == 1: if authorlist_record[0][0] is None: eng.log.error("Error parsing authorlist record for id: %s" % (identifiers,)) authorlist_record = authorlist_record[0][0] # Convert any LaTeX symbols in authornames translate_fieldvalues_from_latex(authorlist_record, '100', code='a') translate_fieldvalues_from_latex(authorlist_record, '700', code='a') # Look for any UNDEFINED fields in authorlist #key = "UNDEFINED" #matching_fields = record_find_matching_fields(key, authorlist_record, tag='100') +\ # record_find_matching_fields(key, authorlist_record, tag='700') #if len(matching_fields) > 0: # UNDEFINED found. Create ticket in author queue # ticketid = create_authorlist_ticket(matching_fields, \ # identifiers, arguments.get('a_rt-queue')) # if ticketid: # eng.log.info("authorlist RT ticket %d submitted for %s" % (ticketid, identifiers)) # else: # eng.log.error("Error while submitting RT ticket for %s" % (identifiers,)) updated_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<collection>\n' + record_xml_output(authorlist_record) \ + '</collection>' if not None == updated_xml: - from invenio.base.records.api import create_record # We store the path to the directory the tarball contents live # Read and grab MARCXML from plotextractor run - new_dict_representation = create_record(updated_xml).dumps() + new_dict_representation = create_record(updated_xml, master_format="marc").dumps() obj.data['authors'] = new_dict_representation["authors"] obj.data['number_of_authors'] = new_dict_representation["number_of_authors"] obj.add_task_result("authors", new_dict_representation["authors"]) obj.add_task_result("number_of_authors", new_dict_representation["number_of_authors"]) author_list.__id__ = "u" def upload_step(obj, eng): """ Perform the upload step. """ uploaded_task_ids = [] #Work comment: # #Prepare in case of filtering the files to up, #no filtering, no other things to do new_dict_representation = Record(obj.data) marcxml_value = new_dict_representation.legacy_export_as_marc() task_id = None # Get a random sequence ID that will allow for the tasks to be # run in order, regardless if parallel task execution is activated sequence_id = random.randrange(1, 60000) task_sleep_now_if_required() extract_path = make_single_directory(CFG_TMPSHAREDDIR, eng.uuid) # Now we launch BibUpload tasks for the final MARCXML files filepath = extract_path + os.sep + str(obj.id) file_fd = open(filepath, 'w') file_fd.write(marcxml_value) file_fd.close() mode = ["-r", "-i"] arguments = obj.extra_data["_repository"]["arguments"] if os.path.exists(filepath): try: args = mode if sequence_id: args.extend(['-I', str(sequence_id)]) if arguments.get('u_name', ""): args.extend(['-N', arguments.get('u_name', "")]) if arguments.get('u_priority', 5): args.extend(['-P', str(arguments.get('u_priority', 5))]) args.append(filepath) task_id = task_low_level_submission("bibupload", "oaiharvest", *tuple(args)) create_oaiharvest_log_str(task_id, obj.extra_data["_repository"]["id"], marcxml_value) except Exception as msg: eng.log.error("An exception during submitting oaiharvest task occured : %s " % (str(msg))) return None else: eng.log.error("marcxmlfile %s does not exist" % (filepath,)) if task_id is None: eng.log.error("an error occurred while uploading %s from %s" % (filepath, obj.extra_data["_repository"]["name"])) else: uploaded_task_ids.append(task_id) eng.log.info("material harvested from source %s was successfully uploaded" % (obj.extra_data["_repository"]["name"],)) if CFG_INSPIRE_SITE: # Launch BibIndex,Webcoll update task to show uploaded content quickly bibindex_params = ['-w', 'collection,reportnumber,global', '-P', '6', '-I', str(sequence_id), '--post-process', 'bst_run_bibtask[taskname="webcoll", user="oaiharvest", P="6", c="HEP"]'] task_low_level_submission("bibindex", "oaiharvest", *tuple(bibindex_params)) eng.log.info("end of upload") def bibclassify(taxonomy, rebuild_cache=False, no_cache=False, output_mode='text', output_limit=20, spires=False, match_mode='full', with_author_keywords=False, extract_acronyms=False, only_core_tags=False): def _bibclassify(obj, eng): import os.path if not os.path.isfile(taxonomy): eng.log.error("No RDF found, no bibclassify can run") return None from invenio.legacy.bibclassify import api if "_result" not in obj.extra_data: obj.extra_data["_result"] = {} if "pdf" in obj.extra_data["_result"]: obj.extra_data["_result"]["bibclassify"] = api.bibclassify_exhaustive_call(obj.extra_data["_result"]["pdf"], taxonomy, rebuild_cache, no_cache, output_mode, output_limit, spires, match_mode, with_author_keywords, extract_acronyms, only_core_tags ) obj.add_task_result("bibclassify", obj.extra_data["_result"]["bibclassify"]) else: obj.log.error("No classification done due to missing fulltext." "\n You need to get it before! see fulltext task") return _bibclassify diff --git a/invenio/modules/workflows/tasks/simplified_data_tasks.py b/invenio/modules/workflows/tasks/simplified_data_tasks.py index 7ee3ca15d..3b446a2a2 100644 --- a/invenio/modules/workflows/tasks/simplified_data_tasks.py +++ b/invenio/modules/workflows/tasks/simplified_data_tasks.py @@ -1,39 +1,35 @@ ## This file is part of Invenio. ## Copyright (C) 2012 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """Basic simplified data test functions - NOT FOR XML """ -from ..config import CFG_OBJECT_STATUS - def task_a(a): def _task_a(obj, eng): """Function task_a docstring""" eng.log.info("executing task a " + str(a)) obj.data += a return _task_a def task_b(obj, eng): """Function task_b docstring""" eng.log.info("executing task b") if obj.data < 20: - obj.change_status(CFG_OBJECT_STATUS.ERROR) - eng.log.info("Object status %s" % (obj.db_obj.status,)) eng.log.info("data < 20") obj.add_task_result("task_b", {'a': 12, 'b': 13, 'c': 14}) eng.halt("Value of filed: data in object is too small.") diff --git a/invenio/modules/workflows/tasks/test_tasks.py b/invenio/modules/workflows/tasks/test_tasks.py index 7f88fe3fa..ee56ca090 100644 --- a/invenio/modules/workflows/tasks/test_tasks.py +++ b/invenio/modules/workflows/tasks/test_tasks.py @@ -1,104 +1,101 @@ ## This file is part of Invenio. ## Copyright (C) 2012 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """Basic test functions - NOT FOR XML """ import time -from ..config import CFG_OBJECT_STATUS def task_a(a): def _task_a(obj, eng): """Function task_a docstring""" eng.log.info("executing task a " + str(a)) obj.data['data'] += a if a > 5: obj.extra_data['redis_search']['publisher'] = "CERN" else: obj.extra_data['redis_search']['publisher'] = "Desy" return _task_a def task_b(obj, eng): """Function task_b docstring""" eng.log.info("executing task b") if obj.data['data'] < 20: - obj.change_status(CFG_OBJECT_STATUS.ERROR) - eng.log.info("Object status %s" % (obj.status,)) eng.log.info("data < 20") obj.add_task_result("task_b", {'a': 12, 'b': 13, 'c': 14}) obj.extra_data['redis_search']['category'] = "lower_than_20" eng.halt("Value of filed: data in object is too small.") else: obj.extra_data['redis_search']['category'] = "higher_than_20" def add_metadata(): def _add_metadata(obj, eng): eng.log.info("executing task add_metadata on obj.type %s" % (obj.content_type,)) if(obj['content_type'] == 'book'): obj.add_field("meta1", "elefant") else: obj.add_field("meta1", "hippo") return _add_metadata def simple_task(times): def _simple_task(obj, eng): a = times eng.log.info("Running simple task %i times" % (times,)) while a > 0: obj.data['data'] -= 1 a -= 1 return _simple_task def sleep_task(t): def _sleep_task(dummy_obj, eng): eng.log.info("Going to sleep...") time.sleep(t) eng.log.info("I've woken up :)") return _sleep_task def lower_than_20(obj, eng): """Function checks if variable is lower than 20""" if obj.data['data'] < 20: eng.log.info("data < 20") eng.halt("Value of filed: a in object is lower than 20.") def higher_than_20(obj, eng): """Function checks if variable is higher than 20""" if obj.data['data'] > 20: eng.log.info("data > 20") eng.halt("Value of filed: a in object is higher than 20.") def add(value): def _add(obj, dummy_eng): """Function adds value to variable""" obj.data['data'] += value return _add def subtract(value): def _subtract(obj, dummy_eng): """Function subtract value from variable""" obj.data['data'] -= value return _subtract diff --git a/invenio/modules/workflows/tasks/workflows_tasks.py b/invenio/modules/workflows/tasks/workflows_tasks.py index 9ccdc5560..d4fb55e3f 100644 --- a/invenio/modules/workflows/tasks/workflows_tasks.py +++ b/invenio/modules/workflows/tasks/workflows_tasks.py @@ -1,282 +1,297 @@ +# -*- coding: utf-8 -*- +## This file is part of Invenio. +## Copyright (C) 2013,2014 CERN. +## +## Invenio is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 2 of t +## License, or (at your option) any later version. +## +## Invenio is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111 1307, USA. + from invenio.modules.workflows.models import (BibWorkflowObject, - BibWorkflowEngineLog, - DATA_TYPES - ) + BibWorkflowEngineLog) from invenio.modules.workflows.api import start_delayed - from invenio.modules.workflows.errors import WorkflowError from time import sleep def get_nb_workflow_created(obj, eng): try: return eng.extra_data["_nb_workflow"] except KeyError: return "0" def num_workflow_running_greater(num): def _num_workflow_running_greater(obj, eng): try: if (eng.extra_data["_nb_workflow"] - eng.extra_data["_nb_workflow_finish"]) > num: return True else: return False except KeyError: return False return _num_workflow_running_greater def get_nb_workflow_running(obj, eng): try: eng.log.error(str()) return eng.extra_data["_nb_workflow"] - eng.extra_data["_nb_workflow_finish"] except KeyError: return "0" def start_workflow(workflow_to_run="default", data=None, copy=True, **kwargs): """ This function allow you to run a new asynchronous workflow, this will be run on the celery node configurer into invenio configuration. The first argument is the name of the workflow. The second one is the data to use for this workflow The copy parameter allow you to pass to the workflow a copy of the obj at the moment of the call . **kargs allow you to add some key:value into the extra data of the object. """ def _start_workflow(obj, eng): myobject = BibWorkflowObject() if copy is True: myobject.copy(obj) if data is not None: myobject.data = data extra = myobject.get_extra_data() myobject.set_extra_data(extra) - myobject.data_type = DATA_TYPES.RECORD + myobject.data_type = "record" myobject.save() workflow_id = start_delayed(workflow_to_run, data=[myobject], stop_on_error=True, **kwargs) eng.log.info("Workflow launched") try: eng.extra_data["_workflow_ids"].append(workflow_id) except KeyError: eng.extra_data["_workflow_ids"] = [workflow_id] try: eng.extra_data["_nb_workflow"] += 1 except KeyError: eng.extra_data["_nb_workflow"] = 1 if "_nb_workflow_failed" not in eng.extra_data: eng.extra_data["_nb_workflow_failed"] = 0 if "_nb_workflow_finish" not in eng.extra_data: eng.extra_data["_nb_workflow_finish"] = 0 if "_uuid_workflow_crashed" not in eng.extra_data: eng.extra_data["_uuid_workflow_crashed"] = [] return _start_workflow def wait_for_workflows_to_complete(obj, eng): """ This function wait all the asynchronous workflow launched. It acts like a barrier """ if '_workflow_ids' in eng.extra_data: for workflow_id in eng.extra_data['_workflow_ids']: try: workflow_id.get() eng.extra_data["_nb_workflow_finish"] += 1 except WorkflowError as e: eng.log.error(str(e)) workflowlog = BibWorkflowEngineLog.query.filter( BibWorkflowEngineLog.id_object == e.id_workflow ).filter(BibWorkflowEngineLog.log_type == 40).all() for log in workflowlog: eng.log.error(log.message) eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 except Exception as e: eng.log.error("Error: Workflow failed %s" % str(e)) eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 else: eng.extra_data["_nb_workflow"] = 0 eng.extra_data["_nb_workflow_failed"] = 0 eng.extra_data["_nb_workflow_finish"] = 0 def wait_for_a_workflow_to_complete_obj(obj, eng): """ This function wait for the asynchronous workflow specified in obj.data ( asyncresult ) It acts like a barrier """ if not obj.data: eng.extra_data["_nb_workflow"] = 0 eng.extra_data["_nb_workflow_failed"] = 0 eng.extra_data["_nb_workflow_finish"] = 0 return None try: obj.data.get() eng.extra_data["_nb_workflow_finish"] += 1 except WorkflowError as e: eng.log.error("Error: Workflow failed %s" % str(e)) workflowlog = BibWorkflowEngineLog.query.filter( BibWorkflowEngineLog.id_object == e.id_workflow ).filter(BibWorkflowEngineLog.log_type == 40).all() for log in workflowlog: eng.log.error(log.message) eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 except Exception as e: eng.log.error("Error: Workflow failed %s" % str(e)) eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 def wait_for_a_workflow_to_complete(obj, eng): """ This function wait for the asynchronous workflow specified in obj.data ( asyncresult ) It acts like a barrier """ if '_workflow_ids' in eng.extra_data: to_wait = None i = 0 while not to_wait and len(eng.extra_data["_workflow_ids"]) > 0: for i in range(0, len(eng.extra_data["_workflow_ids"])): if eng.extra_data["_workflow_ids"][i].status == "SUCCESS": to_wait = eng.extra_data["_workflow_ids"][i] break if eng.extra_data["_workflow_ids"][i].status == "FAILURE": to_wait = eng.extra_data["_workflow_ids"][i] break sleep(1) if not to_wait: return None try: to_wait.get() eng.extra_data["_nb_workflow_finish"] += 1 except WorkflowError as e: eng.extra_data["_uuid_workflow_crashed"].append(e.id_workflow) eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 except: eng.extra_data["_nb_workflow_failed"] += 1 eng.extra_data["_nb_workflow_finish"] += 1 del eng.extra_data["_workflow_ids"][i] else: eng.extra_data["_nb_workflow"] = 0 eng.extra_data["_nb_workflow_failed"] = 0 eng.extra_data["_nb_workflow_finish"] = 0 def write_something_generic(messagea, func): """ This function allows to send a message to bibsched... This messages will be store into log. """ def _write_something_generic(obj, eng): if isinstance(messagea, basestring): if isinstance(func, list): for function in func: function(messagea) else: func(messagea) return None if not isinstance(messagea, list): if callable(messagea): func_messagea = messagea while callable(func_messagea): func_messagea = func_messagea(obj, eng) if isinstance(func, list): for function in func: function(func_messagea) else: func(func_messagea) return None if len(messagea) > 0: temp = "" for func_messagea in messagea: if callable(func_messagea): while callable(func_messagea): func_messagea = func_messagea(obj, eng) temp += str(func_messagea) elif isinstance(func_messagea, basestring): temp += func_messagea if isinstance(func, list): for function in func: function(temp) else: func(temp) return None return _write_something_generic def get_list_of_workflows_to_wait(obj, eng): """ Return a list of asyncresult corresponding to running asynchrnous workflow """ return eng.extra_data["_workflow_ids"] def get_status_async_result_obj_data(obj, eng): return obj.data.state def get_workflows_progress(obj, eng): try: return (eng.extra_data["_nb_workflow_finish"] * 100.0) / (eng.extra_data["_nb_workflow"]) except KeyError: return "No progress (key missing)" except ZeroDivisionError: return "No workflows" def workflows_reviews(stop_if_error=False): def _workflows_reviews(obj, eng): """ This function write a little report about asynchronous workflows in this main workflow Raise an exception if a workflow is gone rogue """ if eng.extra_data["_nb_workflow"] == 0: raise WorkflowError("Nothing has been harvested ! Look into logs for errors !", eng.uuid, obj.id) eng.log.info("%s / %s failed" % (eng.extra_data["_nb_workflow_failed"], eng.extra_data["_nb_workflow"])) if eng.extra_data["_nb_workflow_failed"] and stop_if_error: raise WorkflowError( "%s / %s failed" % (eng.extra_data["_nb_workflow_failed"], eng.extra_data["_nb_workflow"]), eng.uuid, obj.id, payload=eng.extra_data["_uuid_workflow_crashed"]) return _workflows_reviews def log_info(message): def _log_info(obj, eng): eng.log.info(message) return _log_info diff --git a/invenio/modules/workflows/templates/workflows/edit_field_macro.html b/invenio/modules/workflows/templates/workflows/edit_field_macro.html new file mode 100644 index 000000000..c8e6ff185 --- /dev/null +++ b/invenio/modules/workflows/templates/workflows/edit_field_macro.html @@ -0,0 +1,44 @@ +{% macro render_field(field) %} + <div class="form-group"> + {% if field.name != "core" %} + {% if field.name != "submit" %} + {{field.label}} + {% endif %} + <div class="col-sm-10"> + {{ field(**kwargs)|safe }} + </div> + {% else %} + <div class="col-sm-10"> + <div class="checkbox"> + <label> + <b>Core</b> {{field(**kwargs)|safe}} + </label> + </div> + </div> + {% endif %} + </div> + {% if field.errors %} + <ul class=errors> + {% for error in field.errors %} + <li>{{ error }}</li> + {% endfor %} + </ul> + {% endif %} + </dd> +{% endmacro %} + +<!-- div class="form-group"> + <label for="recid" class="col-sm-2 control-label">Rec ID</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="recid" placeholder=""> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label> + <b>Core</b> <input type="checkbox"> + </label> + </div> + </div> + </div> --> \ No newline at end of file diff --git a/invenio/modules/workflows/templates/workflows/entry_details.html b/invenio/modules/workflows/templates/workflows/entry_details.html index 02913912b..277b9ac7e 100644 --- a/invenio/modules/workflows/templates/workflows/entry_details.html +++ b/invenio/modules/workflows/templates/workflows/entry_details.html @@ -1,81 +1,81 @@ {% extends "workflows/layout_workflow_details.html" %} {% import 'workflows/utils.html' as utils %} {% js url_for('workflows.static', filename='js/workflows/entry_details.js'), '50-workflows' %} {% block javascript %} {{ super() }} <script type="text/javascript"> $(document).ready(function(){ bind_object_preview("{{ url_for('workflows.entry_data_preview') }}", "{{ entry.id }}"); }); </script> {% endblock javascript %} {% block body %} <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h3 id="myModalLabel">{{ entry.id }}</h3> </div> <div class="modal-body"> <div class="row"> <div class="col-md-12"> Owner: <b>{{ entry.id_user }}</b> | Creation date: <b>{{ entry.created }}</b> </div> </div> <div class="row"> <div class="col-md-5"> Preview: <div class="well"> <div class="btn-group" name="object_preview_btn" data-toggle="buttons"> <button class="btn btn-xs btn-primary active" name="hd">HTML</button> <button class="btn btn-xs btn-primary" name="marcxml">MARCXML</button> <button class="btn btn-xs btn-primary" name="xm">MARC</button> </div> <div class="btn-group pull-right"> <button class="btn btn-xs">Show PDF</button> <button class="btn btn-xs">Show log</button> </div> <div name="object_preview">{{- data_preview|safe -}}</div> </div> </div> <div class="col-md-3"> Workflow definition: <div class="well"> - {{ utils.function_display(workflow_func, entry.get_extra_data()['task_counter']) }} + {{ utils.function_display(workflow_func, entry.get_extra_data()['_task_counter']) }} </div> </div> <div class="col-md-4"> Messages: <div class="well"> <h4>Matching is finished. Confirm match!</h4> <p>There are 3 possible matches.</p> <a href="#" class="btn">Resolve match <i class="glyphicon glyphicon-zoom-in"></i></a> </div> </div> </div> <div class="row"> <div class="col-md-12"> Keywords: <form> <textarea id="entry_keywords" placeholder="Your keywords here...">ELECTRON POSITRON: COLLIDING BEAMS | COLLIDING BEAMS: ELECTRON POSITRON | ELECTRON POSITRON: ANNIHILATION | ANNIHILATION: ELECTRON POSITRON | JET: HADROPRODUCTION | HADROPRODUCTION: JET | HADRON: MULTIPLE PRODUCTION | MULTIPLE PRODUCTION: HADRON | QUARK: JET | charm | bottom | LEPTON: DIRECT PRODUCTION | DIRECT PRODUCTION: LEPTON | QUARK: SEMILEPTONIC DECAY | SEMILEPTONIC DECAY: QUARK | QUARK: NONLEPTONIC DECAY | NONLEPTONIC DECAY: QUARK | CHARGED PARTICLE: MULTIPLICITY | MARK II | EXPERIMENTAL RESULTS | SLAC PEP STOR | 29 GEV-CMS</textarea> </form> </div> </div> <div class="row"> <div class="col-md-12"> Log: <pre>{{ log }}</pre> </div> </div> </div> <div class="modal-footer"> <a class="btn btn-primary" href="{{ url_for('holdingpen.details', bwobject_id=entry.id)}}" >Open in HoldingPen <i class="glyphicon glyphicon-wrench"></i></a> <button class="btn btn-success">Apply <i class="glyphicon glyphicon-ok"></i></button> <button class="btn btn-danger">Delete <i class="icon-remove icon-white"></i></button> <button class="btn">Edit <i class="glyphicon glyphicon-edit"></i></button> <button id="extra" class="btn">Assign ticket <i class="glyphicon glyphicon-flag"></i></button> <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> </div> {% endblock %} diff --git a/invenio/modules/workflows/templates/workflows/hp_approval_widget.html b/invenio/modules/workflows/templates/workflows/hp_approval_widget.html index 20cec233b..d5b777f50 100644 --- a/invenio/modules/workflows/templates/workflows/hp_approval_widget.html +++ b/invenio/modules/workflows/templates/workflows/hp_approval_widget.html @@ -1,139 +1,90 @@ -{% extends "workflows/hp_layout.html" %} +{% extends "workflows/hp_detailed_layout.html" %} {% import 'workflows/hp_utils.html' as utils %} {%- block header%} {{ super() }} {% css url_for('static', filename='css/prism.css') %} {%- endblock header %} {% js url_for('static', filename='js/prism.js'), '40-workflows' %} {% js url_for('workflows.static', filename='js/workflows/hp_details.js'), '50-workflows' %} {% js url_for('workflows.static', filename='js/workflows/widgets/approval.js'), '50-workflows' %} {% js url_for('workflows.static', filename='js/workflows/hp_datapreview.js'), '50-workflows' %} {%- block javascript %} {{ super() }} <script type="text/javascript"> $(document).ready(function(){ var url_preview = "{{ url_for('holdingpen.entry_data_preview') }}"; - set_url_preview(url_preview); + $('button.preview').click(function() { + bwoid = $(this).attr('data-id'); + format = $(this).attr('name'); + data_preview(url_preview, bwoid, format); + $('button.preview').each(function() { + $(this).removeClass('active'); + }); + $(this).addClass('active'); + }); + + $('button.preview.active').each(function() { + bwoid = $(this).attr('data-id'); + format = $(this).attr('name'); + data_preview(url_preview, bwoid, format); + }); }); </script> {%- endblock javascript %} {% block hpbody %} - <div class="row"> - <div class="col-md-4 pull-left"><a href="{{url_for('holdingpen.maintable')}}"><span class="glyphicon glyphicon-hand-left"></span> Back to Main Table</a></div> - <div class="col-md-4"><h1>Approval Widget</h1></div> - </div> +<div class="container" style="padding-left:0"> + <div id="goodbye-msg" class="col-md-12 text-center"> + </div> - <div class="container"> - <div id="goodbye-msg" class="col-md-12 text-center"> - </div> - {% for i in range(obj_number) %} - {% set bwobject = bwobject_list[i] %} - {% set bwparent = bwparent_list[i] %} - {% set data_preview = data_preview_list[i] %} - {% set workflow = w_metadata_list[i] %} - {% set workflow_func = workflow_func_list[i] %} - {% set info = info_list[i] %} - {% set log = logtext_list[i] %} - <div id="row_fluid{{i}}" class="row"> - <div class="col-md-9"> - <div class="btn-group" name="object_preview_btn" data-toggle="buttons-radio"> - <button class="btn btn-xs btn-primary active" name="hd" onclick="setDataPreview('hd', {{bwobject.id}})">HTML</button> - <button class="btn btn-xs btn-primary" name="marcxml" onclick="setDataPreview('xm', {{bwobject.id}})">MARCXML</button> - <button class="btn btn-xs btn-primary" name="xm" onclick="setDataPreview('tm', {{bwobject.id}})">MARC</button> - <!-- <button class="preview btn btn-xs btn-primary active" name="hd">HTML</button> - <button class="preview btn btn-xs btn-primary" name="xm">MARCXML</button> - <button class="preview btn btn-xs btn-primary" name="tm">MARC</button> --> - </div> - <div class="btn-group pull" name="data_version" data-toggle="buttons-radio"> - <button class="btn btn-xs active" name="initial" onclick="setbwoid({{bwparent.id}})">Initial</button> - {% if bwobject.version == 1 %} - <button class="btn btn-xs" name="error" onclick="setbwoid({{bwobject.id}})">Error</button> - {% else %} - <button class="btn btn-xs disabled" name="error">Error</button> - {% endif %} - {% if bwobject.version == 2 %} - <button class="btn btn-xs" name="final" onclick="setbwoid({{bwobject.id}})">Final</button> - {% else %} - <button class="btn btn-mini disabled" name="final">Final</button> - {% endif %} - </div> - <div id="object_preview_container{{bwobject.id}}" class="object_preview_container"> - {{ data_preview|safe }} - </div> - </div> - <div class="col-md-3"> - <div class="details_link"> - <a href="/admin/holdingpen/details?bwobject_id={{bwobject.id}}"><button class="btn btn-primary" type="button"><i class="icon-eye-open icon-white"></i> Record Details</button></a> - </div> - <div class="well"> - <div class="muted"><b>Workflow Definition:</b></div> - <b>{{workflow.name}}</b> - {{ utils.function_display(workflow_func, bwobject.get_extra_data()['last_task_name'], bwobject.version) }} - </div> - - <div class="well"> - <div class="muted"><b>Error Message:</b></div> - {% if bwobject.get_extra_data()['error_msg'] != "" %} - {{ info['last task name'] }} {{ bwobject.get_extra_data()['error_msg'] }} - <div class="text-right"> - <h6 id="show-more" class="text-right"><a href="#moreinfoModal" role="button" data-toggle="modal" class="float-right">Show More</a> - </h6> - </div> - {% else %} - There were no errors. - {% endif %} - - <div id="moreinfoModal" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 id="myModalLabel">Error Log</h3> - </div> - <div class="modal-body"> - {% for logobj in logtext_list %} - {{logobj}} - {% endfor %} - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> - </div> - </div> - </div> - - <div class="well"> - <div class="muted"><b>Record Metadata:</b></div></br> - <table class="table table-striped"> - <tbody> - {% for key, value in info.items() %} - <b>{{ key }}</b> = {{ value }} - <hr> - {% endfor %} +{% for i in range(obj_number) %} +{% set bwobject = bwobject_list[i] %} +{% set bwparent = bwparent_list[i] %} +{% set data_preview = data_preview_list[i] %} +{% set workflow = w_metadata_list[i] %} +{% set workflow_func = workflow_func_list[i] %} +{% set info = info_list[i] %} +{% set log = logtext_list[i] %} - {% if bwobject.version == 1 %} - <span class="badge badge-success">Final</span> - {% elif bwobject.version == 2 %} - <span class="badge badge-warning">Halted</span> - {% else %} - <span class="badge badge-info">Initial</span> - {% endif %} - </tbody> - </table> - </div> - </div> - </div> + {% block navbar_right scoped %} + <ul class="nav navbar-nav pull-right"> + <li> + <a href="{{ url_for('holdingpen.details', objectid=bwobject.id) }}" ><i class="icon-wrenchwhite icon-wrench"></i>More details</a> + </li> + </ul> + {% endblock navbar_right %} + {% block hp_navbar scoped%} + {{ super() }} + {% endblock hp_navbar %} + + <div id="row_fluid{{i}}" class="row"> + <div class="col-md-3"> <div id="decision-btns{{i}}" class="row decision-btns"> - <form id="form{{i}}" class="theform" method="POST" name="{{ url_for('holdingpen.resolve_widget', bwobject_id=bwobject.id, widget='approval_widget') }}"> + <form id="form{{i}}" class="theform" method="POST" name="{{ url_for('holdingpen.resolve_widget', object_id=bwobject.id, widget='approval_widget') }}"> {% for field in widget %} {{ field }} {% endfor %} </form> </div> - <hr id="hr{{i}}"> - {% endfor %} + </br> + + {% block hpbody_details scoped %} + {{ super() }} + {% endblock hpbody_details %} + + </div> + + {% block hp_preview scoped %} + {{ super() }} + {% endblock hp_preview %} </div> + + <hr id="hr{{i}}"> + {% endfor %} +</div> {% endblock %} diff --git a/invenio/modules/workflows/templates/workflows/hp_detailed_layout.html b/invenio/modules/workflows/templates/workflows/hp_detailed_layout.html new file mode 100644 index 000000000..667eef161 --- /dev/null +++ b/invenio/modules/workflows/templates/workflows/hp_detailed_layout.html @@ -0,0 +1,137 @@ +{%- css url_for('workflows.static', filename='css/workflows/style.css'), '20-workflows' -%} +{% extends "workflows/hp_layout.html" %} + +{% block hpbody_details %} + <div class="well"> + <div class="muted">Workflow: <b>{{workflow.name}}</b></div> + <span type="button" class="pull-right" style="cursor:pointer" data-toggle="collapse" data-target="#workflow-definition"><span class="caret"></span></span> + <div id="workflow-definition" class="collapse"> + <table class="table table-striped"> + <tbody> + <div class="muted"><b>Workflow Tasks:</b></div> + {{ utils.function_display(workflow_func, bwobject.get_extra_data()['last_task_name'], bwobject.version) }} + </tbody> + </table> + </div> + </div> + + <div class="well"> + <div class="muted"><b>Error Message:</b></div> + {% if bwobject.get_extra_data()['error_msg'] != "" %} + {{ info['last task name'] }} {{ bwobject.get_extra_data()['error_msg'] }} + <div class="text-right"> + <h6 id="show-more" class="text-right"><a href="#moreinfoModal" role="button" data-toggle="modal" class="float-right">Show More</a> + </h6> + </div> + {% else %} + There were no errors. + {% endif %} + + <div id="moreinfoModal" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 id="myModalLabel">Error Log</h3> + </div> + <div class="modal-body"> + {% for key, value in log.iteritems() %} + {{key}}: {{value}} + {% endfor %} + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> + </div> + </div> + </div> + + <div class="well"> + <div class="muted"><b>Record Metadata</b> + <span type="button" class="pull-right" style="cursor:pointer" data-toggle="collapse" data-target="#record-metadata-table"><span class="caret"></span></span> + </div> + </br> + <div id="record-metadata-table" class="collapse"> + <table class="table table-striped"> + <tbody> + <!-- {% block record_metadata_table %} --> + {% for key, value in info.iteritems() %} + <b>{{ key }}</b> = {{ value }} + <hr> + {% endfor %} + + {% if bwobject.version == 1 %} + <span class="label label-success">Final</span> + {% elif bwobject.version == 2 %} + <span class="label label-warning">Halted</span> + {% else %} + <span class="label label-info">Initial</span> + {% endif %} + <!-- {% endblock record_metadata_table %} --> + </tbody> + </table> + </div> + </div> +{% endblock %} + +{% block hp_navbar %} + <nav class="navbar navbar-default navbar-static-top" role="navigation"> + <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> + <ul class="nav navbar-nav"> + <li><a href="{{url_for('holdingpen.maintable')}}" class="navbar-btn-sm"><span class="glyphicon glyphicon-hand-left"></span> Back</a> + </li> + <li class="active"> + <a href="#">Details</a> + </li> + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown">Actions<b class="caret"></b></a> + <ul class="dropdown-menu" role="menu"> + <li><button id="restart_button" class="btn btn-sm btn-primary btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-repeat"></i>Restart</button></li> + <li><button id="restart_button_prev" class="btn btn-sm btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-step-backward"></i>Restart Task</button></li> + <li><button id="continue_button" class="btn btn-sm btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-step-forward"></i>Skip Task</button></li> + <li><a id="delete_btn" class="btn btn-sm btn-block btn-danger" data-toggle="modal" data-target="#confirmationModal" role="button"><i class="icon-trash"></i>Delete</a></li> + </ul> + </li> + + <div id="confirmationModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 id="myModalLabel">Please Confirm</h3> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete record #{{bwobject.id}}</p> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button> + <a class="btn btn-danger" href="{{ url_for('holdingpen.delete_from_db', bwobject_id=bwobject.id) }}" >Delete Record</a> + </div> + </div> + </div> + </div> + </ul> + {% block navbar_right %} + {% endblock navbar_right %} + </div> + </nav> +{% endblock %} + + +{% block hp_preview %} + <div class="col-md-9"> + Show as: + <div class="btn-group" name="object_preview_btn" data-toggle="buttons-radio"> + + <button class="preview btn btn-xs btn-primary active" + data-id="{{ bwobject.id }}" + name="hd">HTML</button> + <button class="preview btn btn-xs btn-primary" + data-id="{{ bwobject.id }}" + name="xm">MARCXML</button> + <button class="preview btn btn-xs btn-primary" + data-id="{{ bwobject.id }}" + name="tm">MARC</button> + </div> + <div id="object_preview_container{{bwobject.id}}" class="object_preview_container"> + {{ data_preview|safe }} + </div> + </div> +{% endblock %} diff --git a/invenio/modules/workflows/templates/workflows/hp_details.html b/invenio/modules/workflows/templates/workflows/hp_details.html index 5fc6e3bf2..58398774f 100644 --- a/invenio/modules/workflows/templates/workflows/hp_details.html +++ b/invenio/modules/workflows/templates/workflows/hp_details.html @@ -1,198 +1,102 @@ -{% extends "workflows/hp_layout.html" %} +{% extends "workflows/hp_detailed_layout.html" %} {% import 'workflows/hp_utils.html' as utils %} +{% import 'workflows/edit_field_macro.html' as field_macro %} {% block header%} {{ super() }} {% css url_for('static', filename='css/prism.css') %} {% endblock header %} {% js url_for('static', filename='js/prism.js'), '40-workflows' %} {% js url_for('workflows.static', filename='js/workflows/hp_details.js'), '50-workflows' %} {% js url_for('workflows.static', filename='js/workflows/hp_datapreview.js'), '50-workflows' %} {% block javascript %} {{ super() }} <script type="text/javascript"> $(document).ready(function(){ var bwoid = "{{bwobject.id}}"; var format = "hd"; - var url_preview = "{{ url_for('holdingpen.entry_data_preview') }}"; - var url_restart_record = "{{ url_for('holdingpen.restart_record') }}"; - var url_restart_record_prev = "{{ url_for('holdingpen.restart_record_prev') }}"; - var url_continue_record = "{{ url_for('holdingpen.continue_record') }}"; + url = new Object(); + url.url_preview = "{{ url_for('holdingpen.entry_data_preview') }}"; + url.url_restart_record = "{{ url_for('holdingpen.restart_record') }}"; + url.url_restart_record_prev = "{{ url_for('holdingpen.restart_record_prev') }}"; + url.url_continue_record = "{{ url_for('holdingpen.continue_record') }}"; + url.url_resolve_edit = "{{ url_for('holdingpen.resolve_edit') }}" $('button.preview').click(function() { + bwoid = $(this).attr('data-id'); format = $(this).attr('name'); - data_preview(url_preview, bwoid, format); + data_preview(url.url_preview, bwoid, format); + $('button.preview').each(function() { + $(this).removeClass('active'); + }); + $(this).addClass('active'); }); - data_preview(url_preview, bwoid, format); - action_buttons(url_restart_record, url_restart_record_prev, url_continue_record); + data_preview(url.url_preview, bwoid, format); + action_buttons(url, bwoid); }); </script> {% endblock javascript %} {% block hpbody %} - <nav class="navbar navbar-default navbar-static-top" role="navigation"> - <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> - <ul class="nav navbar-nav"> - <li><a href="{{url_for('holdingpen.maintable')}}" class="navbar-btn-sm"><span class="glyphicon glyphicon-hand-left"></span> Back</a> - </li> - <li class="active"> - <a href="#">Details</a> - </li> - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown">Actions<b class="caret"></b></a> - <ul class="dropdown-menu" role="menu"> - <li><button id="restart_button" class="btn btn-sm btn-primary btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-repeat"></i>Restart</button></li> - <li><button id="restart_button_prev" class="btn btn-sm btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-step-backward"></i>Restart Task</button></li> - <li><button id="continue_button" class="btn btn-sm btn-block" rel="popover" name="{{ bwobject.id }}"/><i class="icon-step-forward"></i>Skip Task</button></li> - <li><a id="delete_btn" class="btn btn-sm btn-block btn-danger" data-toggle="modal" data-target="#confirmationModal" role="button"><i class="icon-trash"></i>Delete</a></li> - </ul> - </li> - - <div id="confirmationModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 id="myModalLabel">Please Confirm</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete record #{{bwobject.id}}</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button> - <a class="btn btn-danger" href="{{ url_for('holdingpen.delete_from_db', bwobject_id=bwobject.id) }}" >Delete Record</a> - </div> - </div> - </div> - </div> - - </ul> + + {% block navbar_right %} <ul class="nav navbar-nav pull-right"> <li> {% if bwobject.get_extra_data()['widget'] %} - <a href="{{ url_for('holdingpen.show_widget', bwobject_id=bwobject.id, widget=info['widget']) }}" ><i class="icon-wrenchwhite icon-wrench"></i>Widget</a> + <a href="{{ url_for('holdingpen.show_widget', objectid=bwobject.id) }}" ><i class="icon-wrenchwhite icon-wrench"></i>Action needed</a> {% else %} {% endif %} </li> - </ul> - </div> - </nav> + </ul> + {% endblock navbar_right %} + + {% block hp_navbar %} + {{ super() }} + {% endblock hp_navbar %} <div class="container" style="padding-left:0px;"> <div class="row"> - <div class="col-md-3"> - + <div class="col-md-3"> <div class="well"> - <div class="muted">Workflow: <b>{{workflow.name}}</b></div> - <span type="button" class="pull-right" style="cursor:pointer" data-toggle="collapse" data-target="#workflow-definition"><span class="caret"></span></span> - <div id="workflow-definition" class="collapse"> - <table class="table table-striped"> - <tbody> - <div class="muted"><b>Workflow Tasks:</b></div> - {{ utils.function_display(workflow_func, bwobject.get_extra_data()['last_task_name'], bwobject.version) }} - </tbody> - </table> - </div> - </div> - - <div class="well"> - <div class="muted"><b>Error Message:</b></div> - {% if bwobject.get_extra_data()['error_msg'] != "" %} - {{ info['last task name'] }} {{ bwobject.get_extra_data()['error_msg'] }} - <div class="text-right"> - <h6 id="show-more" class="text-right"><a href="#moreinfoModal" role="button" data-toggle="modal" class="pull-right">Show More</a> - </h6> - </div> - {% else %} - There were no errors. - {% endif %} - - <div id="moreinfoModal" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 id="myModalLabel">Error Log</h3> - </div> - <div class="modal-body"> - {% for key, value in log.items() %} - {{key}}: {{value}} + <form id="edit_form" class="form-horizontal" role="form" method="POST" name=""> + {% for field in edit_record_widget %} + {{ field_macro.render_field(field) }} {% endfor %} - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button> - </div> - </div> + </form> </div> - <div class="well"> - <div class="muted"><b>Record Metadata</b> - <span type="button" class="pull-right" style="cursor:pointer" data-toggle="collapse" data-target="#record-metadata-table"><span class="caret"></span></span> - </div> - </br> - <div id="record-metadata-table" class="collapse"> - <table class="table table-striped"> - <tbody> - {% for key, value in info.items() %} - <b>{{ key }}</b> = {{ value }} - <hr> - {% endfor %} + {% block hpbody_details %} + {{ super() }} + {% endblock hpbody_details %} - {% if bwobject.version == 1 %} - <span class="badge badge-success">Final</span> - {% elif bwobject.version == 2 %} - <span class="badge badge-warning">Halted</span> - {% else %} - <span class="badge badge-info">Initial</span> - {% endif %} - </tbody> - </table> - </div> - </div> </div> - <div class="col-md-9"> - Show as: - <div class="btn-group" name="object_preview_btn" data-toggle="buttons-radio"> - <button class="preview btn btn-xs btn-primary active" name="hd">HTML</button> - <button class="preview btn btn-xs btn-primary" name="xm">MARCXML</button> - <button class="preview btn btn-xs btn-primary" name="tm">MARC</button> - </div> - <!-- <div class="btn-group pull" name="data_version" data-toggle="buttons-radio"> - <button class="btn btn-xs active" name="initial" onclick="">Initial</button> - {% if bwobject.version == 1 %} - <button class="btn btn-xs" name="final" onclick="">Final</button> - {% else %} - <button class="btn btn-xs disabled" name="final">Final</button> - {% endif %} - {% if bwobject.version == 2 %} - <button class="btn btn-xs" name="error" onclick="">Error</button> - {% else %} - <button class="btn btn-xs disabled" name="error">Error</button> - {% endif %} - </div> --> + <div class="col-md-9"> + {% if 'message' in bwobject.get_extra_data() and bwobject.get_extra_data()['widget'] != None %} <div id="usermessage" class="alert alert-warning alert-dismissable"> {{bwobject.get_extra_data()['message']}} <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> </div> {% endif %} + {% block hp_preview %} + {{ super() }} + {% endblock %} + {% if bwobject.version == 1 %} <div id="successmessage" class="alert alert-success"> Record ran workflow successfully. <a class="close" data-dismiss="alert">×</a> </div> {% endif %} - - <div id="object_preview_container{{bwobject.id}}"> - {{ data_preview|safe }} - </div> </div> </div> </div> {% endblock %} diff --git a/invenio/modules/workflows/templates/workflows/hp_edit_record_widget.html b/invenio/modules/workflows/templates/workflows/hp_edit_record_widget.html new file mode 100644 index 000000000..57c4d2e48 --- /dev/null +++ b/invenio/modules/workflows/templates/workflows/hp_edit_record_widget.html @@ -0,0 +1,36 @@ +{%- css url_for('workflows.static', filename='css/workflows/style.css'), '20-workflows' -%} + +{% block hp_edit_record_widget %} + +<form id="edit_form" class="form-horizontal" role="form" method="POST" name=""> + <div class="form-group"> + <label for="recid" class="col-sm-2 control-label">Rec ID</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="recid" placeholder=""> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label> + <b>Core</b> <input type="checkbox"> + </label> + </div> + </div> + </div> + <div class="form-group"> + <label for="recid" class="col-sm-2 control-label">Field Code</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="recid" placeholder=""> + </div> + </div> + <div class="form-group"> + <label for="recid" class="col-sm-2 control-label">Type Code</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="recid" placeholder=""> + </div> + </div> + <button id="submit-edit-record-widget" type="submit" class="btn btn-default">Submit</button> +</form> + +{% endblock %} \ No newline at end of file diff --git a/invenio/modules/workflows/templates/workflows/hp_index.html b/invenio/modules/workflows/templates/workflows/hp_index.html index b90c1ac9a..6a7fef3a0 100644 --- a/invenio/modules/workflows/templates/workflows/hp_index.html +++ b/invenio/modules/workflows/templates/workflows/hp_index.html @@ -1,65 +1,21 @@ {% extends "workflows/hp_layout.html" %} {% block hpbody %} <h1>HoldingPen Overview</h1> <div class="container"> <div class="col-md-3"> <h4>Main Table</h4> <div><a href="{{url_for('holdingpen.maintable')}}"><button class="btn btn-primary" type="button">Show Table</button></a></div> </div> <div class="col-md-3"> <h4>Pending Tasks</h4> - {% for task in tasks.iterkeys() %} - <div class="task-btns"><a href="{{url_for('holdingpen.maintable')}}"><button class="btn btn-warning" type="button">{{task}}: {{tasks[task][0]}}</button></a></br></div> + {% for task, object_count in tasks.iteritems() %} + <div class="task-btns"><a href="{{url_for('holdingpen.maintable')}}"><button class="btn btn-warning" type="button">{{task}}: {{object_count}}</button></a></br></div> {% endfor %} </div> <div class="col-md-3"> <h4>Assigned to me</h4> <div><a href="{{url_for('holdingpen.maintable')}}"><button class="btn btn-primary" type="button">Show Records</button></a></div> </div> </div> - - - <style type="text/css" title="currentStyle"> - @import "{{ url_for('workflows.static', filename='css/workflows/DT_bootstrap.css') }}"; - </style> - - <h1>Records in Holding Pen</h1> - - <button id="refresh_button" class="btn btn-primary pull-right">Refresh!</button> - - <div class="container"> - <div class="row"> - <table id="example" cellpadding="0" cellspacing="0" border="0" class="table table-striped table-bordered"> - <thead> - <tr> - <th>Id</th> - <th>Title</th> - <th>Source</th> - <th>Category</th> - <th>Workflow ID</th> - <th>Owner</th> - <th>Created</th> - <th>Version</th> - <th>Details</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - <tr> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - <td></td> - </tr> - </tbody> - </table> - </div> - </div> {% endblock %} diff --git a/invenio/modules/workflows/templates/workflows/hp_maintable.html b/invenio/modules/workflows/templates/workflows/hp_maintable.html index 15e5229b0..7ce16eb8a 100644 --- a/invenio/modules/workflows/templates/workflows/hp_maintable.html +++ b/invenio/modules/workflows/templates/workflows/hp_maintable.html @@ -1,115 +1,118 @@ {% extends "workflows/hp_layout.html" %} {% import 'workflows/hp_utils.html' as utils %} {% block header%} {{ super() }} {% css url_for('static', filename='css/ColVis.css') %} {% css url_for('static', filename='css/dataTables.bootstrap.css') %} + {% css url_for('workflows.static', filename='css/workflows/style.css'), '20-workflows' %} {% endblock header %} {% js url_for('static', filename='js/jquery.dataTables.min.js'), '30-datatables' %} {% js url_for('static', filename='js/ColVis.js'), '30-datatables' %} +{% js url_for('static', filename='js/bootstrap.js'), '30-datatables' %} {% js url_for('static', filename='js/dataTables.bootstrap.js'), '30-datatables' %} +{% js url_for('static', filename='js/bootstrap-tagsinput.min.js'), '30-datatables' %} {% js url_for('workflows.static', filename='js/workflows/hp_maintable.js'), '50-workflows' %} {% js url_for('workflows.static', filename='js/workflows/widgets/approval.js'), '50-workflows' %} {% block javascript %} {{ super() }} <script type="text/javascript"> $(document).ready(function(){ url = new Object(); + // console.log(url); url.load_table = "{{ url_for('holdingpen.load_table')|safe }}"; url.batch_widget = "{{ url_for('holdingpen.batch_widget') }}"; url.resolve_widget = "{{ url_for('holdingpen.resolve_widget') }}"; url.delete_single = "{{ url_for('holdingpen.delete_from_db') }}"; url.refresh = "{{ url_for('holdingpen.refresh') }}"; url.widget = "{{ url_for('holdingpen.show_widget') }}"; url.details = "{{ url_for('holdingpen.details')|safe }}"; url.batch_widget = "{{ url_for('holdingpen.batch_widget') }}"; + url.preview = "{{ url_for('holdingpen.entry_data_preview') }}"; + version_showing = "{{version_showing}}"; init_urls(url); init_urls_approval(url); + oTable = init_datatable(version_showing); }); </script> {% endblock javascript %} {% block hpbody %} <div id="alert-message"></div> <nav class="navbar navbar-default navbar-static-top" role="navigation"> <!-- Brand and toggle get grouped for better mobile display --> <!-- Collect the nav links, forms, and other content for toggling --> - <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> + <div id="bs-example-navbar-collapse-1" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> - <li class="active"> - <a href="#">Home</a> - </li> <!-- <li><a href="#">Assigned to me</a></li> --> <p class="navbar-text" style="margin-right:0px; color:light gray;">Show Records by:</p> <li class="dropdown"> - <a class="dropdown-toggle" data-toggle="dropdown" href="#">Actions<b class="caret"></b></a> - <ul class="dropdown-menu"> - {% set i = 0 %} - {% for task in widget_list.iterkeys() %} - <li><a id="task{{i}}" class="task-btn" tabindex="-1" name="{{task}}">{{task}}: {{widget_list[task][0]}}</a></li> - {% set i = i+1 %} - {% endfor %} - </ul> + <a class="dropdown-toggle dropdown-headline" data-toggle="dropdown" href="#">Actions<b class="caret"></b></a> + <ul class="dropdown-menu"> + {% if user_list %} + {% set i = 0 %} + {% for task, object_count in widget_list.iteritems() %} + <li><a id="task{{i}}" class="task-btn" tabindex="-1" name="{{task}}">{{task}}: {{object_count}}</a></li> + {% set i = i+1 %} + {% endfor %} + {% else %} + <li><a class="task-btn">No actions left</a></li> + {% endif %} + </ul> </li> <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown">Status<b class="caret"></b></a> + <a href="#" class="dropdown-toggle dropdown-headline" data-toggle="dropdown">Status<b class="caret"></b></a> <ul class="dropdown-menu" role="menu"> <li><a id="version-halted" class="version-selection" name="Halted">Halted</a></li> - <li><a id="version-final" class="version-selection" name="Final">Final</a></li> + <li><a id="version-final" class="version-selection" name="Final">Completed</a></li> <li><a id="version-running" class="version-selection" name="Running">Running</a></li> </ul> </li> </ul> - <ul class="nav navbar-nav pull-right"> - <li><button id="refresh_button" type="button" class="btn btn-default navbar-btn">Refresh!</button></li> - </ul> </div> </nav> - <div id="tag-area" class="row"> - </div> + <input id="tagsinput" type="text" data-role="tagsinput" placeholder="Add new tag"/> + </br> <div class="container"> <div class="row"> - <table id="example" cellpadding="0" cellspacing="0" border="0" class="table table-bordered"> + <table id="maintable" cellpadding="0" cellspacing="0" border="0" class="table table-bordered"> <thead> <tr> - <th><input id="select-all" type="checkbox"></th> <th>Id</th> + <th><input id="select-all" type="checkbox"></th> <th>Title</th> <th>Source</th> <th>Category</th> - <th>Workflow ID</th> - <th>Owner</th> <th>Created</th> - <th>Version</th> + <th>Status</th> + <th>Type</th> <th>Details</th> <th>Actions</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> <td></td> - <td></td> </tr> </tbody> </table> </div> </div> {% endblock %} \ No newline at end of file diff --git a/invenio/modules/workflows/templates/workflows/hp_utils.html b/invenio/modules/workflows/templates/workflows/hp_utils.html index 6807bb7cc..cc72efc4d 100644 --- a/invenio/modules/workflows/templates/workflows/hp_utils.html +++ b/invenio/modules/workflows/templates/workflows/hp_utils.html @@ -1,51 +1,68 @@ {% macro function_display(fun, task_counter, version) -%} <!-- {% set comma2 = joiner(",") %} --> {% set i = 0 %} {% set flag = 0 %} {% for w in fun %} {% if w is not iterable %} - <!-- {{ comma2() }} --> - </br> - {% if w.__title__ == task_counter and version == 2 %} - <b> - {% endif %} - - <a href="#infoModal{{i}}" data-toggle="modal">{{ w.__title__ }}</a> - - {% if w.__title__ == task_counter %} + {{ function_display_inner(w, task_counter, version, flag) }} + {% if w.func_name == task_counter and version == 2 %} {% set flag = 1 %} - {% if version == 2 or version == 3 %} - </b><i class="icon-remove"></i> - {% else %} - <i class="icon-ok"></i> - {% endif %} - {% else %} - {% if flag == 0 %} - <i class="icon-ok"></i> - {% endif %} - {% endif %} - - <div id="infoModal{{i}}" class="modal hide fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 id="myModalLabel">{{ w.__title__ }}</h3> - </div> - <div class="modal-body"> - {% if w.func_closure is defined %} - {% if w.func_closure is iterable %} - {% set comma = joiner(", ") %} - {% for arg in w.func_closure %} - {{ comma() }} - <b>Parameter: {{ arg.cell_contents }}</b> - {% endfor %} - </br> - {% endif %} - {% endif %} - {{ w.func_doc }} - </div> - </div> - {% set i = i + 1 %} + {% else %} + {{ function_display(w, task_counter, version) }} {% endif %} + <!-- {{ comma2() }} --> + {% set i = i + 1 %} {% endfor %} {%- endmacro %} + +{% macro function_display_inner(w, task_counter, version, flag) -%} + </br> + + {% if flag == 0 %} + + <button type="button" class="btn btn-default btn-sm" href="#" data-toggle="modal" data-target="#infoModal{{i}}"> + <span class="glyphicon glyphicon-ok-sign"></span> + {% if w.func_name == task_counter and version == 2 %} + <strong>{{ w.func_name }}</strong> + {% else %} + {{ w.func_name }} + {% endif %} + </button> + + {% else %} + + <button type="button" class="btn btn-default btn-sm" href="#" data-toggle="modal" data-target="#infoModal{{i}}"> + {{ w.func_name }} + </button> + + {% endif %} + + <div class="modal fade" id="infoModal{{i}}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title" id="myModalLabel">{{ w.func_name }}</h4> + </div> + <div class="modal-body"> + {% if w.func_closure is defined %} + {% if w.func_closure is iterable %} + {% set comma = joiner(", ") %} + {% for arg in w.func_closure %} + {{ comma() }} + <b>Parameter: {{ arg.cell_contents }}</b> + {% endfor %} + </br> + {% endif %} + {% endif %} + {{ w.func_doc }} + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> + </div> + </div><!-- /.modal-content --> + </div><!-- /.modal-dialog --> + </div><!-- /.modal --> + +{%- endmacro %} diff --git a/invenio/modules/workflows/templates/workflows/row_formatter.html b/invenio/modules/workflows/templates/workflows/row_formatter.html index ae64d7c15..5f88f8509 100644 --- a/invenio/modules/workflows/templates/workflows/row_formatter.html +++ b/invenio/modules/workflows/templates/workflows/row_formatter.html @@ -1,46 +1,48 @@ -#checkbox$ -<input type="checkbox"> -#id$ -{{record.id}} -#title$ -{% if record.get_extra_data()['redis_search'].has_key('title') %}{{ record.get_extra_data()['redis_search']['title'] }} -{% else %}None -{% endif %} -#source$ -{% if record.get_extra_data()['redis_search'].has_key('source') %}{{ record.get_extra_data()['redis_search']['source'] }} -{% else %}None -{% endif %} -#category$ -{% if record.get_extra_data()['redis_search'].has_key('category') %}{{ record.get_extra_data()['redis_search']['category'] }} -{% else %}None -{% endif %} -#workflow_id$ -{{record.id_workflow}} -#owner$ -{% if record.get_extra_data()['owner'] == None %}None{% endif %} -#pretty_date$ -{{ pretty_date(record.created)}} -#version$ -{% if record.version == 1 %}<span class="label label-success">Final</span> -{% elif record.version == 2 %}<span class="label label-warning">Halted</span> +<td id="id"> +{{object.id}} +</td> +<td id="checkbox"> +<input type="checkbox" class="hp-check"> +</td> +<td id="title"> +{{ record.get('title', {}) }} +</td> +<td id="source"> +{{ extra_data.get('source', "No source") }} +</td> +<td id="category"> +{{ ", ".join(categories) or "No category" }} +</td> +<td id="pretty_date"> +{{ pretty_date(object.created)}} +</td> +<td id="version"> +{% if object.version == 1 %}<span class="label label-success">Completed</span> +{% elif object.version == 2 %}<span class="label label-warning">Halted</span> {% else %}<span class="label label-info">Running</span> {% endif %} -#details$ +</td> +<td id="type"> +{{object.data_type}} +</td> +<td id="details"> {% if widget == None %} - <a href="{{url_for('holdingpen.details', bwobject_id=record.id)}}">Details</a> + <a href="{{url_for('holdingpen.details', objectid=object.id)}}">Details</a> {% else %} {% set widget_instance = widget() %} - <a href="{{url_for('holdingpen.show_widget', bwobject_id=record.id, widget=widget_instance.__class__.__name__)}}">Details</a> + <a href="{{url_for('holdingpen.show_widget', objectid=object.id)}}">Details</a> {% endif %} -#widget$ +</td> +<td id="widget"> {% if widget != None %} - {% set widget = widget() %} - {% if mini_widget != None %} - {% set mini_widget = mini_widget() %} - {% for field in mini_widget %} - {{ field }} - {% endfor %} - {% endif %} + {% set widget = widget() %} + {% if mini_widget != None %} + {% set mini_widget = mini_widget() %} + {% for field in mini_widget %} + {{ field(objectid=object.id) }} + {% endfor %} + {% endif %} {% else %} N/A -{% endif %} \ No newline at end of file +{% endif %} +</td> \ No newline at end of file diff --git a/invenio/modules/workflows/testsuite/test_halt.py b/invenio/modules/workflows/testsuite/test_halt.py index bb391e187..51c6a5992 100644 --- a/invenio/modules/workflows/testsuite/test_halt.py +++ b/invenio/modules/workflows/testsuite/test_halt.py @@ -1,84 +1,83 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """Unit tests for workflows views.""" from __future__ import absolute_import from invenio.testsuite import InvenioTestCase, make_test_suite, \ run_test_suite import logging class WorkflowTestBranch(InvenioTestCase): def test_halt(self): from invenio.modules.workflows.loader import workflows from invenio.modules.workflows.api import start - from invenio.modules.workflows.config import CFG_OBJECT_VERSION, \ - CFG_WORKFLOW_STATUS - from invenio.modules.workflows.models import BibWorkflowObjectLog + from invenio.modules.workflows.engine import WorkflowStatus + from invenio.modules.workflows.models import (BibWorkflowObjectLog, + ObjectVersion) halt_engine = lambda obj, eng: eng.halt("Test") class HaltTest(object): workflow = [halt_engine] workflows['halttest'] = HaltTest data = [{'somekey', 'somevalue'}, ] eng = start('halttest', data) idx, obj = list(eng.getObjects())[0] - assert obj.version == CFG_OBJECT_VERSION.HALTED - assert eng.status == CFG_WORKFLOW_STATUS.FINISHED + assert obj.version == ObjectVersion.HALTED + assert eng.status == WorkflowStatus.FINISHED assert BibWorkflowObjectLog.get( id_object=obj.id, log_type=logging.ERROR).count() == 0 def test_halt_in_branch(self): from workflow.patterns import IF_ELSE from invenio.modules.workflows.loader import workflows from invenio.modules.workflows.api import start - from invenio.modules.workflows.config import CFG_OBJECT_VERSION, \ - CFG_WORKFLOW_STATUS - from invenio.modules.workflows.models import BibWorkflowObjectLog - + from invenio.modules.workflows.engine import WorkflowStatus + from invenio.modules.workflows.models import (BibWorkflowObjectLog, + ObjectVersion) always_true = lambda obj, eng: True halt_engine = lambda obj, eng: eng.halt("Test") class BranchTest(object): workflow = [ IF_ELSE(always_true, [halt_engine], [halt_engine]) ] workflows['branchtest'] = BranchTest data = [{'somekey', 'somevalue'}, ] eng = start('branchtest', data) idx, obj = list(eng.getObjects())[0] - assert obj.version == CFG_OBJECT_VERSION.HALTED - assert eng.status == CFG_WORKFLOW_STATUS.FINISHED + assert obj.version == ObjectVersion.HALTED + assert eng.status == WorkflowStatus.FINISHED assert BibWorkflowObjectLog.get( id_object=obj.id, log_type=logging.ERROR).count() == 0 TEST_SUITE = make_test_suite(WorkflowTestBranch) if __name__ == "__main__": run_test_suite(TEST_SUITE) diff --git a/invenio/modules/workflows/utils.py b/invenio/modules/workflows/utils.py index f593bed8d..05405b3f5 100644 --- a/invenio/modules/workflows/utils.py +++ b/invenio/modules/workflows/utils.py @@ -1,280 +1,279 @@ ## -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. from __future__ import print_function import re import redis import traceback from six import iteritems from invenio.ext.logging import register_exception from .errors import WorkflowDefinitionError REGEXP_RECORD = re.compile("<record.*?>(.*?)</record>", re.DOTALL) class BibWorkflowObjectIdContainer(object): """ This class is only made to be able to store a workflow ID and to retrieve easily the workflow from this ID. It is used maily to overide some problem with SQLAlchemy when we change of execution thread ( for example Celery ) """ def __init__(self, bibworkflowobject=None): if bibworkflowobject is not None: self.id = bibworkflowobject.id else: self.id = None def get_object(self): from invenio.modules.workflows.models import BibWorkflowObject if self.id is not None: return BibWorkflowObject.query.filter(BibWorkflowObject.id == self.id).one() else: return None def from_dict(self, dict_to_process): self.id = dict_to_process[str(self.__class__)]["id"] return self def to_dict(self): return {str(self.__class__): self.__dict__} - def __str__(self): return "BibWorkflowObject(%s)" % (str(self.id),) class WorkflowsTaskResult(object): """ Class to contain the current task results. """ def __init__(self, task_name, name, result): self.task_name = task_name self.name = name self.result = result def get_workflow_definition(name): """ Tries to load the given workflow from the system. """ from .loader import workflows try: return workflows[name] except Exception as e: raise WorkflowDefinitionError("Error with workflow '%s': %s\n%s" % (name, str(e), traceback.format_exc()), workflow_name=name) def determineDataType(data): # If data is a dictionary and contains type key, # we can directly derive the data_type if isinstance(data, dict): if 'type' in data: data_type = data['type'] else: data_type = 'dict' else: # If data is not a dictionary, we try to guess MIME type # by using magic library try: from magic import Magic mime_checker = Magic(mime=True) data_type = mime_checker.from_buffer(data) # noqa except: register_exception(stream="warning", prefix= "BibWorkflowObject.determineDataType:" + " Impossible to resolve data type.") data_type = "" return data_type ## TODO special thanks to http://code.activestate.com/recipes/440514-dictproperty-properties-for-dictionary-attributes/ class dictproperty(object): class _proxy(object): def __init__(self, obj, fget, fset, fdel): self._obj = obj self._fget = fget self._fset = fset self._fdel = fdel def __getitem__(self, key): try: return self._fget(self._obj, key) except TypeError: print("can't read item") def __setitem__(self, key, value): try: self._fset(self._obj, key, value) except TypeError: print("can't set item %s: %s" % (str(key), str(value),)) def __delitem__(self, key): try: self._fdel(self._obj, key) except TypeError: print("can't delete item") def __init__(self, fget=None, fset=None, fdel=None, doc=None): self._fget = fget self._fset = fset self._fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self return self._proxy(obj, self._fget, self._fset, self._fdel) def redis_create_search_entry(bwobject): redis_server = set_up_redis() extra_data = bwobject.get_extra_data() #creates database entries to not loose key value pairs in redis for key, value in iteritems(extra_data["redis_search"]): redis_server.sadd("holdingpen_sort", str(key)) redis_server.sadd("holdingpen_sort:%s" % (str(key),), str(value)) redis_server.sadd("holdingpen_sort:%s:%s" % (str(key), str(value),), bwobject.id) redis_server.sadd("holdingpen_sort", "owner") redis_server.sadd("holdingpen_sort:owner", extra_data['owner']) redis_server.sadd("holdingpen_sort:owner:%s" % (extra_data['owner'],), bwobject.id) redis_server.sadd("holdingpen_sort:last_task_name:%s" % (extra_data['_last_task_name'],), bwobject.id) def filter_holdingpen_results(key, *args): """Function filters holdingpen entries by given key: value pair. It returns list of IDs.""" redis_server = set_up_redis() new_args = [] for a in args: new_args.append("holdingpen_sort:" + a) return redis_server.sinter("holdingpen_sort:" + key, *new_args) def get_redis_keys(key=None): redis_server = set_up_redis() if key: return list(redis_server.smembers("holdingpen_sort:%s" % (str(key),))) else: return list(redis_server.smembers("holdingpen_sort")) def get_redis_values(key): redis_server = set_up_redis() return redis_server.smembers("holdingpen_sort:%s" % (str(key),)) def set_up_redis(): """ Sets up the redis server for the saving of the HPContainers @return: Redis server object. """ from flask import current_app redis_server = redis.Redis.from_url( current_app.config.get('CACHE_REDIS_URL', 'redis://localhost:6379') ) return redis_server def empty_redis(): redis_server = set_up_redis() redis_server.flushall() def sort_bwolist(bwolist, iSortCol_0, sSortDir_0): if iSortCol_0 == 0: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.id, reverse=True) else: bwolist.sort(key=lambda x: x.id, reverse=False) elif iSortCol_0 == 1: pass # if sSortDir_0 == 'desc': # bwolist.sort(key=lambda x: x.id_user, reverse=True) # else: # bwolist.sort(key=lambda x: x.id_user, reverse=False) elif iSortCol_0 == 2: pass # if sSortDir_0 == 'desc': # bwolist.sort(key=lambda x: x.id_user, reverse=True) # else: # bwolist.sort(key=lambda x: x.id_user, reverse=False) elif iSortCol_0 == 3: pass # if sSortDir_0 == 'desc': # bwolist.sort(key=lambda x: x.id_user, reverse=True) # else: # bwolist.sort(key=lambda x: x.id_user, reverse=False) elif iSortCol_0 == 4: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.id_workflow, reverse=True) else: bwolist.sort(key=lambda x: x.id_workflow, reverse=False) elif iSortCol_0 == 5: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.id_user, reverse=True) else: bwolist.sort(key=lambda x: x.id_user, reverse=False) elif iSortCol_0 == 6: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.created, reverse=True) else: bwolist.sort(key=lambda x: x.created, reverse=False) elif iSortCol_0 == 7: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.version, reverse=True) else: bwolist.sort(key=lambda x: x.version, reverse=False) elif iSortCol_0 == 8: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.version, reverse=True) else: bwolist.sort(key=lambda x: x.version, reverse=False) elif iSortCol_0 == 9: if sSortDir_0 == 'desc': bwolist.sort(key=lambda x: x.version, reverse=True) else: bwolist.sort(key=lambda x: x.version, reverse=False) return bwolist def parse_bwids(bwolist): import ast return list(ast.literal_eval(bwolist)) diff --git a/invenio/modules/workflows/views/holdingpen.py b/invenio/modules/workflows/views/holdingpen.py index 0a75868f1..8e7f7dc4a 100644 --- a/invenio/modules/workflows/views/holdingpen.py +++ b/invenio/modules/workflows/views/holdingpen.py @@ -1,502 +1,535 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. -## Copyright (C) 2013 CERN. +## Copyright (C) 2013, 2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """Holding Pen web interface""" -from __future__ import print_function +import re -from flask import render_template, Blueprint, redirect, url_for, flash, request, current_app, jsonify +from flask import (render_template, Blueprint, + request, current_app, + jsonify, session) from flask.ext.login import login_required +from flask.ext.breadcrumbs import default_breadcrumb_root, register_breadcrumb +from flask.ext.menu import register_menu -from ..models import BibWorkflowObject, Workflow -from ..loader import widgets from invenio.base.decorators import templated, wash_arguments -from invenio.modules.formatter.engine import format_record from invenio.base.i18n import _ -from flask.ext.breadcrumbs import default_breadcrumb_root, register_breadcrumb -from flask.ext.menu import register_menu from invenio.utils.date import pretty_date + +from ..models import BibWorkflowObject, Workflow, ObjectVersion +from ..loader import widgets from ..utils import (get_workflow_definition, sort_bwolist) from ..api import continue_oid_delayed, start -from ..config import CFG_OBJECT_VERSION blueprint = Blueprint('holdingpen', __name__, url_prefix="/admin/holdingpen", template_folder='../templates', static_folder='../static') default_breadcrumb_root(blueprint, '.holdingpen') +REG_TD = re.compile("<td id=\"(.+?)\">(.+?)</td>", re.DOTALL) + @blueprint.route('/', methods=['GET', 'POST']) @blueprint.route('/index', methods=['GET', 'POST']) @login_required @register_menu(blueprint, 'main.admin.holdingpen', _('Holdingpen')) @register_breadcrumb(blueprint, '.', _('Holdingpen')) @templated('workflows/hp_index.html') def index(): """ Displays main interface of Holdingpen. Acts as a hub for catalogers (may be removed) """ - from ..containers import bwolist - - # FIXME: need to autodiscover widgets properly - widget_list = {} - for widget in widgets: - widget_list[widget] = [0, []] + from ..containers import create_hp_containers + bwolist = create_hp_containers() - for bwo in bwolist: - print bwo.get_extra_data()['widget'] - if ('widget' in bwo.get_extra_data()) and \ - (bwo.get_extra_data()['widget'] is not None) \ - and (bwo.version == CFG_OBJECT_VERSION.HALTED): - widget_list[bwo.get_extra_data()['widget']][1].append(bwo) - for key in widget_list: - widget_list[key][0] = len(widget_list[key][1]) + widget_list = get_widget_list(bwolist) return dict(tasks=widget_list) @blueprint.route('/maintable', methods=['GET', 'POST']) @register_breadcrumb(blueprint, '.records', _('Records')) @login_required @templated('workflows/hp_maintable.html') def maintable(): """ Displays main table interface of Holdingpen. """ - from ..containers import bwolist + from ..containers import create_hp_containers + bwolist = create_hp_containers() - # FIXME: need to autodiscover widgets properly - widget_list = {} - for widget in widgets: - import IPython - # IPython.embed() - print widget - widget_list[widgets[widget].__title__] = [0, []] + widget_list = get_widget_list(bwolist) - for bwo in bwolist: - if ('widget' in bwo.get_extra_data()) and \ - (bwo.get_extra_data()['widget'] is not None) \ - and (bwo.version != CFG_OBJECT_VERSION.FINAL): - widget = bwo.get_extra_data()['widget'] - widget_list[widgets[widget].__title__][1].append(bwo) - for key in widget_list: - widget_list[key][0] = len(widget_list[key][1]) + try: + version_showing = current_app.config['VERSION_SHOWING'] + except KeyError: + version_showing = ObjectVersion.HALTED - return dict(bwolist=bwolist, widget_list=widget_list) + return dict(bwolist=bwolist, widget_list=widget_list, + version_showing=version_showing) @blueprint.route('/refresh', methods=['GET', 'POST']) @login_required def refresh(): """ Reloads the bibworkflow_containers file, thus rebuilding the BWObject list. """ # FIXME: Temp hack until redis is hooked up try: - version_showing=current_app.config['VERSION_SHOWING'] + version_showing = session['VERSION_SHOWING'] load_table(version_showing) except: pass # import invenio.modules.workflows.containers # reload(invenio.modules.workflows.containers) return 'Records Refreshed' @blueprint.route('/batch_widget', methods=['GET', 'POST']) @login_required @wash_arguments({'bwolist': (unicode, "")}) def batch_widget(bwolist): """ Renders widget accepting single or multiple records. """ from ..utils import parse_bwids bwolist = parse_bwids(bwolist) try: bwolist = map(int, bwolist) except ValueError: - print('Error in IDs') + # Bad ID, we just pass for now + pass objlist = [] - workflow_list = [] workflow_func_list = [] w_metadata_list = [] info_list = [] widgetlist = [] bwo_parent_list = [] logtext_list = [] objlist = [BibWorkflowObject.query.get(i) for i in bwolist] for bwobject in objlist: extracted_data = extract_data(bwobject) bwo_parent_list.append(extracted_data['bwparent']) logtext_list.append(extracted_data['logtext']) info_list.append(extracted_data['info']) w_metadata_list.append(extracted_data['w_metadata']) workflow_func_list.append(extracted_data['workflow_func']) if bwobject.get_extra_data()['widget'] not in widgetlist: - widgetlist.append(bwobject.get_extra_data()['widget']) + widgetlist.append(bwobject.get_widget()) widget_form = widgets[widgetlist[0]] result = widget_form().render(objlist, bwo_parent_list, info_list, logtext_list, w_metadata_list, workflow_func_list) url, parameters = result return render_template(url, **parameters) @blueprint.route('/load_table', methods=['GET', 'POST']) @login_required @wash_arguments({'version_showing': (unicode, "default")}) @templated('workflows/hp_maintable.html') def load_table(version_showing): """ Function used for the passing of JSON data to the DataTable + 1] First checks for what record version to show + 2] then sorting direction, + 3] then if the user searched for something + and finally it builds the JSON to send. """ - from ..containers import bwolist - - try: - version_showing = request.get_json() - VERSION_SHOWING = [] - - if version_showing['final'] == True: - VERSION_SHOWING.append(CFG_OBJECT_VERSION.FINAL) - if version_showing['halted'] == True: - VERSION_SHOWING.append(CFG_OBJECT_VERSION.HALTED) - if version_showing['running'] == True: - VERSION_SHOWING.append(CFG_OBJECT_VERSION.RUNNING) - print 'NEW STUFF' + from ..containers import create_hp_containers + VERSION_SHOWING = [] + req = request.get_json() + + if req: + if req.get('final', None): + VERSION_SHOWING.append(ObjectVersion.FINAL) + if req.get('halted', None): + VERSION_SHOWING.append(ObjectVersion.HALTED) + if req.get('running', None): + VERSION_SHOWING.append(ObjectVersion.RUNNING) current_app.config['VERSION_SHOWING'] = VERSION_SHOWING - rebuild_containers = True - except: - print 'OLD STUFFS' - version_showing = request.get_json() + else: try: VERSION_SHOWING = current_app.config['VERSION_SHOWING'] - rebuild_containers = True except: - VERSION_SHOWING = [CFG_OBJECT_VERSION.HALTED] - rebuild_containers = False + VERSION_SHOWING = [ObjectVersion.HALTED] # sSearch will be used for searching later - a_search = request.args.get('sSearch') + a_search = request.args.get('sSearch', None) try: i_sortcol_0 = request.args.get('iSortCol_0') s_sortdir_0 = request.args.get('sSortDir_0') i_display_start = int(request.args.get('iDisplayStart')) i_display_length = int(request.args.get('iDisplayLength')) sEcho = int(request.args.get('sEcho')) + 1 except: - i_sortcol_0 = current_app.config['iSortCol_0'] - s_sortdir_0 = current_app.config['sSortDir_0'] - i_display_start = current_app.config['iDisplayStart'] - i_display_length = current_app.config['iDisplayLength'] - sEcho = current_app.config['sEcho'] + 1 - - if a_search or rebuild_containers: - # FIXME: Temp measure until Redis is hooked up - from ..containers import create_hp_containers - print 'rebuilding containers' - print 'with version:', VERSION_SHOWING - bwolist = create_hp_containers(sSearch=a_search, version_showing=VERSION_SHOWING) + i_sortcol_0 = current_app.config.get('iSortCol_0', 0) + s_sortdir_0 = current_app.config.get('sSortDir_0', None) + i_display_start = current_app.config.get('iDisplayStart', 0) + i_display_length = current_app.config.get('iDisplayLength', 10) + sEcho = current_app.config.get('sEcho', 0) + 1 + + bwolist = create_hp_containers(sSearch=a_search, + version_showing=VERSION_SHOWING) if 'iSortCol_0' in current_app.config: i_sortcol_0 = int(i_sortcol_0) if i_sortcol_0 != current_app.config['iSortCol_0'] \ or s_sortdir_0 != current_app.config['sSortDir_0']: bwolist = sort_bwolist(bwolist, i_sortcol_0, s_sortdir_0) current_app.config['iDisplayStart'] = i_display_start current_app.config['iDisplayLength'] = i_display_length current_app.config['iSortCol_0'] = i_sortcol_0 current_app.config['sSortDir_0'] = s_sortdir_0 current_app.config['sEcho'] = sEcho table_data = { "aaData": [] } - table_data['iTotalRecords'] = len(bwolist) - table_data['iTotalDisplayRecords'] = len(bwolist) - #This will be simplified once Redis is utilized. - - rendered_rows = [] + try: + table_data['iTotalRecords'] = len(bwolist) + table_data['iTotalDisplayRecords'] = len(bwolist) + except: + bwolist = create_hp_containers(version_showing=VERSION_SHOWING) + table_data['iTotalRecords'] = len(bwolist) + table_data['iTotalDisplayRecords'] = len(bwolist) + + # This will be simplified once Redis is utilized. records_showing = 0 for bwo in bwolist[i_display_start:i_display_start+i_display_length]: try: - widgetname = widgets[bwo.get_extra_data()['widget']].__title__ + # FIXME: Will be used in near future + # widgetname = widgets[bwo.get_extra_data()['widget']].__title__ widget = widgets[bwo.get_extra_data()['widget']] except KeyError: - widgetname = None + # widgetname = None widget = None # if widget != None and bwo.version in VERSION_SHOWING: records_showing += 1 mini_widget = getattr(widget, "mini_widget", None) - row = render_template('workflows/row_formatter.html', record=bwo, - widget=widget, mini_widget=mini_widget, - pretty_date=pretty_date) + record = bwo.get_data() + if not isinstance(record, dict): + record = {} + extra_data = bwo.get_extra_data() + category_list = record.get('subject_term', []) + if isinstance(category_list, dict): + category_list = [category_list] + categories = ["%s (%s)" % (subject['term'], subject['scheme']) + for subject in category_list] + row = render_template('workflows/row_formatter.html', + object=bwo, + record=record, + extra_data=extra_data, + categories=categories, + widget=widget, + mini_widget=mini_widget, + pretty_date=pretty_date) - list1 = [r.split('$') for r in row.split('#')] d = {} - list1.pop(0) - for key, value in list1: - d[key] = value + for key, value in REG_TD.findall(row): + d[key] = value.strip() table_data['aaData'].append( - [d['checkbox'], - d['id'], + [d['id'], + d['checkbox'], d['title'], d['source'], d['category'], - d['workflow_id'], - d['owner'], d['pretty_date'], d['version'], + d['type'], d['details'], d['widget'] - ]) + ] + ) table_data['sEcho'] = sEcho table_data['iTotalRecords'] = len(bwolist) table_data['iTotalDisplayRecords'] = len(bwolist) + return jsonify(table_data) - print 'LOAD TABLE RETURNING THAT MANY RECORDS:', len(bwolist) - return table_data + +@blueprint.route('/get_version_showing', methods=['GET', 'POST']) +@login_required +def get_version_showing(): + """ + Returns current version showing, saved in current_app.config + """ + try: + return current_app.config['VERSION_SHOWING'] + except KeyError: + return None -@blueprint.route('/details', methods=['GET', 'POST']) -@register_breadcrumb(blueprint, '.details', "Record Details") +@blueprint.route('/details/<objectid>', methods=['GET', 'POST']) +@register_breadcrumb(blueprint, '.details', _("Record Details")) @login_required -@wash_arguments({'bwobject_id': (int, 0)}) -def details(bwobject_id): +def details(objectid): """ Displays info about the object, and presents the data of all available versions of the object. (Initial, Error, Final) """ - bwobject = BibWorkflowObject.query.get(bwobject_id) + of = "hd" + bwobject = BibWorkflowObject.query.filter( + BibWorkflowObject.id == objectid).first_or_404() extracted_data = extract_data(bwobject) - # FIXME: need to determine right format - recformat = "hd" + try: + edit_record_widget = widgets['edit_record_widget']() + except KeyError: + # Could not load edit_record_widget + edit_record_widget = [] return render_template('workflows/hp_details.html', bwobject=bwobject, bwparent=extracted_data['bwparent'], info=extracted_data['info'], log=extracted_data['logtext'], - data_preview=bwobject.get_formatted_data(recformat), + data_preview=bwobject.get_formatted_data(of), workflow_func=extracted_data['workflow_func'], - workflow=extracted_data['w_metadata']) + workflow=extracted_data['w_metadata'], + edit_record_widget=edit_record_widget) @blueprint.route('/restart_record', methods=['GET', 'POST']) @login_required -@wash_arguments({'bwobject_id': (int, 0)}) -def restart_record(bwobject_id, start_point='continue_next'): +@wash_arguments({'objectid': (int, 0)}) +def restart_record(objectid, start_point='continue_next'): """ Restarts the initial object in its workflow """ - bwobject = BibWorkflowObject.query.get(bwobject_id) + bwobject = BibWorkflowObject.query.get(objectid) workflow = Workflow.query.filter( Workflow.uuid == bwobject.id_workflow).first() start(workflow.name, [bwobject.get_data()]) return 'Record Restarted' @blueprint.route('/continue_record', methods=['GET', 'POST']) @login_required -@wash_arguments({'bwobject_id': (int, 0)}) -def continue_record(bwobject_id): +@wash_arguments({'objectid': (int, 0)}) +def continue_record(objectid): """ Restarts the initial object in its workflow """ - continue_oid_delayed(oid=bwobject_id, start_point='continue_next') + continue_oid_delayed(oid=objectid, start_point='continue_next') return 'Record continued workflow' @blueprint.route('/restart_record_prev', methods=['GET', 'POST']) @login_required -@wash_arguments({'bwobject_id': (int, 0)}) -def restart_record_prev(bwobject_id): +@wash_arguments({'objectid': (int, 0)}) +def restart_record_prev(objectid): """ Restarts the initial object in its workflow from the current task """ - continue_oid_delayed(oid=bwobject_id, start_point="restart_task") + continue_oid_delayed(oid=objectid, start_point="restart_task") return 'Record restarted current task' @blueprint.route('/delete_from_db', methods=['GET', 'POST']) @login_required -@wash_arguments({'bwobject_id': (int, 0)}) -def delete_from_db(bwobject_id): +@wash_arguments({'objectid': (int, 0)}) +def delete_from_db(objectid): """ Deletes all available versions of the object from the db """ - # FIXME: Temp hack until redis is hooked up - # import invenio.modules.workflows.containers - _delete_from_db(bwobject_id) - # reload invenio.modules.workflows.containers + BibWorkflowObject.delete(bwobject_id) return 'Record Deleted' -def _delete_from_db(bwobject_id): - from invenio.ext.sqlalchemy import db - # delete every BibWorkflowObject version from the db - # TODO: THIS NEEDS FIXING - print bwobject_id - BibWorkflowObject.query.filter(BibWorkflowObject.id == bwobject_id).delete() - db.session.commit() - - @blueprint.route('/delete_multi', methods=['GET', 'POST']) @login_required @wash_arguments({'bwolist': (unicode, "")}) def delete_multi(bwolist): from ..utils import parse_bwids bwolist = parse_bwids(bwolist) - print 'bwolist:', bwolist - for bwobject_id in bwolist: - print 'bwobject_id:', bwobject_id - _delete_from_db(bwobject_id) + for objectid in bwolist: + delete_from_db(objectid) return 'Records Deleted' -@blueprint.route('/widget', methods=['GET', 'POST']) -@register_breadcrumb(blueprint, '.widget', "Widget") +@blueprint.route('/action/<objectid>', methods=['GET', 'POST']) +@register_breadcrumb(blueprint, '.widget', _("Widget")) @login_required -@wash_arguments({'bwobject_id': (int, 0), - 'widget': (unicode, 'default')}) -def show_widget(bwobject_id, widget): +def show_widget(objectid): """ Renders the widget assigned to a specific record """ - bwobject = BibWorkflowObject.query.get(bwobject_id) + bwobject = BibWorkflowObject.query.filter( + BibWorkflowObject.id == objectid).first_or_404() + + widget = bwobject.get_widget() + # FIXME: add case here if no widget widget_form = widgets[widget] extracted_data = extract_data(bwobject) result = widget_form().render([bwobject], [extracted_data['bwparent']], [extracted_data['info']], [extracted_data['logtext']], [extracted_data['w_metadata']], [extracted_data['workflow_func']]) url, parameters = result return render_template(url, **parameters) -@blueprint.route('/resolve_widget', methods=['POST']) +@blueprint.route('/resolve', methods=['GET', 'POST']) @login_required -@wash_arguments({'bwobject_id': (unicode, '0'), - 'widget': (unicode, 'default')}) -def resolve_widget(bwobject_id, widget): +@wash_arguments({'objectid': (unicode, '-1'), + 'widget': (unicode, 'default')}) +def resolve_widget(objectid, widget): """ Resolves the action taken in a widget. Calls the run_widget function of the specific widget. """ widget_form = widgets[widget] - widget_form().run_widget(bwobject_id, request) + widget_form().run_widget(objectid, request) return "Done" +@blueprint.route('/resolve_edit', methods=['GET', 'POST']) +@login_required +@wash_arguments({'objectid': (unicode, '0'), + 'form': (unicode, '')}) +def resolve_edit(objectid, form): + """ + Performs the changes to the record made in the edit record widget. + """ + if request: + edit_record(request.form) + return 'Record Edited' + + @blueprint.route('/entry_data_preview', methods=['GET', 'POST']) @login_required -@wash_arguments({'oid': (unicode, '0'), - 'recformat': (unicode, None)}) -def entry_data_preview(oid, recformat): +@wash_arguments({'objectid': (unicode, '0'), + 'of': (unicode, None)}) +def entry_data_preview(objectid, of): """ Presents the data in a human readble form or in xml code """ from flask import Markup from pprint import pformat - - bwobject = BibWorkflowObject.query.get(int(oid)) - formatted_data = bwobject.get_formatted_data(recformat) + bwobject = BibWorkflowObject.query.get(int(objectid)) + + formatted_data = bwobject.get_formatted_data(of) if isinstance(formatted_data, dict): formatted_data = pformat(formatted_data) - if recformat and recformat in ("xm", "xml", "marcxml"): + if of and of in ("xm", "xml", "marcxml"): data = Markup.escape(formatted_data) else: data = formatted_data return jsonify(data=data) def get_info(bwobject): """ Parses the hpobject and extracts its info to a dictionary """ info = {} if bwobject.get_extra_data()['owner'] != {}: info['owner'] = bwobject.get_extra_data()['owner'] else: info['owner'] = 'None' info['parent id'] = bwobject.id_parent info['workflow id'] = bwobject.id_workflow info['object id'] = bwobject.id info['widget'] = bwobject.get_extra_data()['widget'] return info def extract_data(bwobject): """ Extracts metadata for BibWorkflowObject needed for rendering the Record's details and widget page. """ extracted_data = {} if bwobject.id_parent is not None: extracted_data['bwparent'] = \ BibWorkflowObject.query.get(bwobject.id_parent) else: extracted_data['bwparent'] = None # TODO: read the logstuff from the db extracted_data['loginfo'] = "" extracted_data['logtext'] = {} for log in extracted_data['loginfo']: extracted_data['logtext'][log.get_extra_data()['last_task_name']] = \ log.message extracted_data['info'] = get_info(bwobject) try: extracted_data['info']['widget'] = bwobject.get_extra_data()['widget'] except (KeyError, AttributeError): pass extracted_data['w_metadata'] = \ Workflow.query.filter(Workflow.uuid == bwobject.id_workflow).first() extracted_data['workflow_func'] = \ get_workflow_definition(extracted_data['w_metadata'].name).workflow return extracted_data + + +def edit_record(form): + """ + Will call the edit record widget resolve function + """ + for key in form.iterkeys(): + # print '%s: %s' % (key, form[key]) + pass + + +def get_widget_list(object_list): + """ + Returns a dicts of widget names mapped to + the number of halted objects associated with that widget. + """ + widget_dict = {} + for bwo in object_list: + widget = bwo.get_widget() + if widget is not None and bwo.version == ObjectVersion.HALTED: + widget_count = widget_dict.setdefault(widget, 0) + widget_count += 1 + return widget_dict diff --git a/invenio/modules/workflows/widgets/approval_widget.py b/invenio/modules/workflows/widgets/approval_widget.py index 020f122a8..4c8d00ba6 100644 --- a/invenio/modules/workflows/widgets/approval_widget.py +++ b/invenio/modules/workflows/widgets/approval_widget.py @@ -1,77 +1,78 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. from ..hp_field_widgets import (bootstrap_accept, bootstrap_accept_mini, bootstrap_reject, bootstrap_reject_mini) from wtforms import SubmitField, Form +from invenio.base.i18n import _ + __all__ = ['approval_widget'] class approval_widget(Form): - reject = SubmitField(label='Reject', widget=bootstrap_reject) - accept = SubmitField(label='Accept', widget=bootstrap_accept) + reject = SubmitField(label=_('Reject'), widget=bootstrap_reject) + accept = SubmitField(label=_('Accept'), widget=bootstrap_accept) class mini_widget(Form): - reject = SubmitField(label='Reject', widget=bootstrap_reject_mini) - accept = SubmitField(label='Accept', widget=bootstrap_accept_mini) + reject = SubmitField(label=_('Reject'), widget=bootstrap_reject_mini) + accept = SubmitField(label=_('Accept'), widget=bootstrap_accept_mini) def render(self, bwobject_list, bwparent_list, info_list, logtext_list, w_metadata_list, workflow_func_list, *args, **kwargs): data_preview_list = [] # setting up approval widget for bwo in bwobject_list: data_preview_list.append(bwo.get_formatted_data()) return ('workflows/hp_approval_widget.html', {'bwobject_list': bwobject_list, 'bwparent_list': bwparent_list, 'widget': approval_widget(), 'data_preview_list': data_preview_list, 'obj_number': len(bwobject_list), 'info_list': info_list, 'logtext_list': logtext_list, 'w_metadata_list': w_metadata_list, 'workflow_func_list': workflow_func_list}) - def run_widget(self, bwobject_id, request): + def run_widget(self, objectid, request): """ Resolves the action taken in the approval widget """ - from flask import request, flash, redirect, url_for + from flask import request, flash from ..api import continue_oid_delayed - from ..views.holdingpen import _delete_from_db from ..models import BibWorkflowObject - bwobject = BibWorkflowObject.query.get(bwobject_id) + bwobject = BibWorkflowObject.query.get(objectid) if request.form['decision'] == 'Accept': bwobject.remove_widget() - continue_oid_delayed(bwobject_id) + continue_oid_delayed(objectid) flash('Record Accepted') - + elif request.form['decision'] == 'Reject': - _delete_from_db(bwobject_id) + BibWorkflowObject.delete(objectid) flash('Record Rejected') approval_widget.__title__ = 'Approve Record' widget = approval_widget() diff --git a/invenio/modules/workflows/widgets/bibmatch_widget.py b/invenio/modules/workflows/widgets/bibmatch_widget.py index 6158bcb3f..c47a31dfe 100644 --- a/invenio/modules/workflows/widgets/bibmatch_widget.py +++ b/invenio/modules/workflows/widgets/bibmatch_widget.py @@ -1,59 +1,54 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. -## Copyright (C) 2012, 2013 CERN. +## Copyright (C) 2012, 2013, 2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +from invenio.base.i18n import _ from ..hp_field_widgets import bootstrap_accept from wtforms import SubmitField, Form __all__ = ['bibmatch_widget'] class bibmatch_widget(Form): - accept = SubmitField(label='Accept', widget=bootstrap_accept) + accept = SubmitField(label=_('Accept'), widget=bootstrap_accept) def render(self, bwobject_list, *args, **kwargs): - from ..models import BibWorkflowObject - from ..views.holdingpen import _entry_data_preview + # FIXME: Currently not working # setting up bibmatch widget bwobject = bwobject_list[0] + results = bwobject.get_extra_data()['_tasks_results'] - try: - matches = bwobject.get_extra_data()['tasks_results']['match_record'] - except: - pass - + matches = [] match_preview = [] - # adding dummy matches - match_preview.append(BibWorkflowObject.query.filter( - BibWorkflowObject.id == bwobject.id).first()) - match_preview.append(BibWorkflowObject.query.filter( - BibWorkflowObject.id == bwobject.id).first()) + for res in results: + if res.name == "matcher": + matches.append(res.result) - data_preview = _entry_data_preview(bwobject.get_data()) + data_preview = None return ('workflows/hp_bibmatch_widget.html', {'bwobject': bwobject, 'widget': bibmatch_widget(), 'match_preview': match_preview, 'matches': matches, 'data_preview': data_preview}) bibmatch_widget.__title__ = 'Bibmatch Widget' widget = bibmatch_widget() diff --git a/invenio/modules/workflows/widgets/edit_record_widget.py b/invenio/modules/workflows/widgets/edit_record_widget.py new file mode 100644 index 000000000..e4dbed0a2 --- /dev/null +++ b/invenio/modules/workflows/widgets/edit_record_widget.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +## +## This file is part of Invenio. +## Copyright (C) 2012, 2013 CERN. +## +## Invenio is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License as +## published by the Free Software Foundation; either version 2 of the +## License, or (at your option) any later version. +## +## Invenio is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Invenio; if not, write to the Free Software Foundation, Inc., +## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +from wtforms import TextField, Form, SubmitField, BooleanField +from ..hp_field_widgets import bootstrap_submit +from flask import render_template + +__all__ = ['edit_record_widget'] + + +class edit_record_widget(Form): + recid = TextField(label='Rec ID') + core = BooleanField(label='Core') + field_code = TextField(label='Field Code') + type_code = TextField(label='Type Code') + submit = SubmitField(label="Submit", widget=bootstrap_submit) + + def render(self, *args, **kwargs): + return render_template('workflows/hp_edit_record_widget.html') + + +edit_record_widget.__title__ = 'Edit Record' + +widget = edit_record_widget() diff --git a/invenio/modules/workflows/worker_engine.py b/invenio/modules/workflows/worker_engine.py index fe29ffee6..43fc99476 100644 --- a/invenio/modules/workflows/worker_engine.py +++ b/invenio/modules/workflows/worker_engine.py @@ -1,185 +1,189 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2012, 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. from invenio.ext.sqlalchemy import db from .client import run_workflow, continue_execution from .engine import BibWorkflowEngine -from .models import BibWorkflowObject, Workflow -from .config import CFG_OBJECT_VERSION - - -class InvenioBibWorkflowValueError(Exception): - pass +from .models import BibWorkflowObject, Workflow, ObjectVersion +from .errors import WorkflowObjectVersionError def run_worker(wname, data, **kwargs): """ Runs workflow with given name and given data. Data can be specified as list of objects or single id of WfeObject/BibWorkflowObjects. """ wfe = BibWorkflowEngine(wname, **kwargs) wfe.save() objects = prepare_objects(data, wfe) run_workflow(wfe=wfe, data=objects, **kwargs) return wfe def restart_worker(wid, **kwargs): """ Restarts workflow with given id (wid) and given data. If data are not specified then it will load all initial data for workflow. Data can be specified as list of objects or single id of WfeObject/BibWorkflowObjects. """ data = BibWorkflowObject.query.filter(BibWorkflowObject.id_workflow == wid, BibWorkflowObject.version == - CFG_OBJECT_VERSION.INITIAL).all() + ObjectVersion.INITIAL).all() workflow = Workflow.query.filter(Workflow.uuid == wid).first() wfe = BibWorkflowEngine(workflow.name, **kwargs) wfe.save() objects = prepare_objects(data, wfe) run_workflow(wfe=wfe, data=objects, **kwargs) return wfe def continue_worker(oid, restart_point="continue_next", **kwargs): """ Restarts workflow with given id (wid) at given point. restart_point can be one of: * restart_prev: will restart from the previous task * continue_next: will continue to the next task * restart_task: will restart the current task """ data = [BibWorkflowObject.query.filter(BibWorkflowObject.id == oid).first()] + #data[0].version = ObjectVersion.RUNNING + #data[0].save() workflow = Workflow.query.filter(Workflow.uuid == data[0].id_workflow).first() wfe = BibWorkflowEngine(workflow.name, uuid=None, id_user=0, workflow_object=workflow, **kwargs) wfe.save() continue_execution(wfe, data, restart_point, **kwargs) return wfe def prepare_objects(data, workflow_object): objects = [] + data_type = workflow_object.get_default_data_type() for obj in data: if isinstance(obj, BibWorkflowObject): if obj.id: - obj.log.debug("Object found for process") + obj.log.info("Object found for process") objects.append(_prepare_objects_helper(obj, workflow_object)) else: + obj.log.info("Object not found for process") objects.append(obj) else: # First we create an initial object for each data item new_initial = \ BibWorkflowObject(id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.INITIAL - ) + version=ObjectVersion.INITIAL, + data_type=data_type) new_initial.set_data(obj) new_initial._update_db() # Then we create another object to actually work on current_obj = BibWorkflowObject(id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.RUNNING, - id_parent=new_initial.id) + version=ObjectVersion.RUNNING, + id_parent=new_initial.id, + data_type=data_type) current_obj.set_data(obj) objects.append(current_obj) return objects + def _prepare_objects_helper(obj, workflow_object): assert obj - if obj.version == CFG_OBJECT_VERSION.INITIAL: - obj.log.debug("State: Initial") + if obj.version == ObjectVersion.INITIAL: + obj.log.info("State: Initial") new_id = obj._create_version_obj(id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.RUNNING, + version=ObjectVersion.RUNNING, id_parent=obj.id, no_update=True) return BibWorkflowObject.query.filter(BibWorkflowObject.id == new_id).first() - elif obj.version in (CFG_OBJECT_VERSION.HALTED, CFG_OBJECT_VERSION.FINAL): - obj.log.debug("State: Halted or Final") + elif obj.version in (ObjectVersion.HALTED, ObjectVersion.FINAL): + obj.log.info("State: Halted or Final") # creating INITIAL object # for FINAL version: maybe it should set # id_parent to the previous final object new_initial = obj._create_version_obj(id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.INITIAL, + version=ObjectVersion.INITIAL, no_update=True) new_id = obj._create_version_obj(id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.RUNNING, + version=ObjectVersion.RUNNING, id_parent=new_initial, no_update=True) return BibWorkflowObject.query.filter(BibWorkflowObject.id == new_id).first() - elif obj.version == CFG_OBJECT_VERSION.RUNNING: + elif obj.version == ObjectVersion.RUNNING: # object shuld be deleted restart from INITIAL - obj.log.debug("State: Running") + obj.log.info("State: Running") if obj.id_workflow is not None: obj.log.info("""WARNING! You want to restart from temporary object. We can't guarantee that data object is not corrupted. Workflow will start from associated INITIAL object and RUNNING object will be deleted.""") parent_obj = BibWorkflowObject.query.filter( BibWorkflowObject.id == obj.id_parent).first() new_initial = parent_obj._create_version_obj( id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.INITIAL, + version=ObjectVersion.INITIAL, no_update=True) new_id = parent_obj._create_version_obj( id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.RUNNING, + version=ObjectVersion.RUNNING, id_parent=new_initial, no_update=True) db.session.delete(obj) return BibWorkflowObject.query.filter(BibWorkflowObject.id == new_id).first() else: - obj.log.info("""You are running workflow on a object created manualy + obj.log.info("""You are running workflow on a object created manually outside of the workflow. Workflow will execute on THIS object (it will change its state and/or data) but it would also create INITIAL version of the object to keep its original state.""") # We assume that there is no parent object, so we create a new # INITIAL object, which will become a parent. new_parent = obj._create_version_obj( id_workflow=workflow_object.uuid, - version=CFG_OBJECT_VERSION.INITIAL, + version=ObjectVersion.INITIAL, no_update=True) # We add an id_workflow to our object obj.id_workflow = workflow_object.uuid obj.id_parent = new_parent obj._update_db() return obj else: - raise InvenioBibWorkflowValueError("Object version is unknown: %s" % - (obj.version,)) + raise WorkflowObjectVersionError("Object version is unknown: %s" % + (obj.version,), + obj_version=obj.version, + id_object=obj.id) diff --git a/invenio/modules/workflows/workflows/full_doc_process.py b/invenio/modules/workflows/workflows/full_doc_process.py index f11c1576d..7f594fa2b 100644 --- a/invenio/modules/workflows/workflows/full_doc_process.py +++ b/invenio/modules/workflows/workflows/full_doc_process.py @@ -1,59 +1,57 @@ +# -*- coding: utf-8 -*- ## This file is part of Invenio. -## Copyright (C) 2012 CERN. +## Copyright (C) 2013,2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111 1307, USA. from ..tasks.marcxml_tasks import (convert_record_with_repository, plot_extract, convert_record_to_bibfield, fulltext_download, refextract, author_list, upload_step, quick_match_record, inspire_filter_custom, bibclassify ) from ..tasks.workflows_tasks import (log_info) from ..tasks.logic_tasks import (workflow_if, workflow_else ) -from ..models import DATA_TYPES - from invenio.config import CFG_PREFIX class full_doc_process(object): - object_type = DATA_TYPES.RECORD + object_type = "record" workflow = [convert_record_with_repository("oaiarXiv2inspire_nofilter.xsl"), convert_record_to_bibfield, - inspire_filter_category(category_widgeted=["gr-qc"], category_accepted=['*'], widget="approval_widget"), workflow_if(quick_match_record, True), [ plot_extract(["latex"]), fulltext_download, inspire_filter_custom(fields=["report_number", "arxiv_category"], custom_accepted=["*"], - widget="approval_widget"), + custom_refused="gr-qc", widget="approval_widget"), bibclassify(taxonomy=CFG_PREFIX + "/etc/bibclassify/HEP.rdf", output_mode="dict", match_mode="partial"), refextract, author_list, upload_step, ], workflow_else, [ log_info("Record already into database"), ], ] diff --git a/invenio/modules/workflows/workflows/generic_harvesting_workflow.py b/invenio/modules/workflows/workflows/generic_harvesting_workflow.py index 27181307d..84497dbcd 100644 --- a/invenio/modules/workflows/workflows/generic_harvesting_workflow.py +++ b/invenio/modules/workflows/workflows/generic_harvesting_workflow.py @@ -1,67 +1,66 @@ +# -*- coding: utf-8 -*- ## This file is part of Invenio. -## Copyright (C) 2012 CERN. +## Copyright (C) 2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of t ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111 1307, USA. """Implements an example of a typical ingestion workflow for MARCXML records""" from ..tasks.marcxml_tasks import (get_repositories_list, init_harvesting, harvest_records, get_files_list, get_eng_uuid_harvested, get_records_from_file ) from ..tasks.workflows_tasks import (start_workflow, wait_for_workflows_to_complete, workflows_reviews, get_nb_workflow_created ) from ..tasks.logic_tasks import (foreach, end_for ) from ..tasks.bibsched_tasks import write_something_bibsched from invenio.base.config import CFG_TMPSHAREDDIR -from ..models import DATA_TYPES - class generic_harvesting_workflow(object): - object_type = DATA_TYPES.HARVEST + object_type = "harvest" workflow = [init_harvesting, foreach(get_repositories_list(['arxivb']), "repository"), [ harvest_records, foreach(get_files_list(CFG_TMPSHAREDDIR, get_eng_uuid_harvested)), [ foreach(get_records_from_file()), [ start_workflow("full_doc_process", None), write_something_bibsched(["Workflow started : ", get_nb_workflow_created, " "]), ], end_for ], end_for ], end_for, wait_for_workflows_to_complete, write_something_bibsched("the end"), - workflows_reviews + workflows_reviews() ]