diff --git a/invenio/modules/workflows/models.py b/invenio/modules/workflows/models.py index c5131986f..a19c49929 100644 --- a/invenio/modules/workflows/models.py +++ b/invenio/modules/workflows/models.py @@ -1,719 +1,717 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## 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. """Models for BibWorkflow Objects.""" import collections import os import tempfile import base64 import logging from six.moves import cPickle from six import string_types, iteritems, callable 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 .utils import (redis_create_search_entry, WorkflowsTaskResult, session_manager) from .logger import get_logger, BibWorkflowLogHandler class ObjectVersion(object): - """Specify the different ObjectVersion possible.""" INITIAL = 0 FINAL = 1 HALTED = 2 RUNNING = 3 MAPPING = {0: "New,", 1: "Done,", 2: "Need action,", - 3: "In process,"} + 3: "In process,"} + REVERSE_MAPPING = {"New": 0, "Done": 1, "Need action": 2, + "In process": 3} def get_default_data(): """Return the base64 representation of the data default value.""" data_default = {} return base64.b64encode(cPickle.dumps(data_default)) def get_default_extra_data(): """Return the base64 representation of the extra_data default value.""" extra_data_default = {"_tasks_results": {}, "owner": {}, "_task_counter": {}, "_error_msg": None, "_last_task_name": "", "latest_object": -1, "_action": None, "redis_search": {}, "source": ""} return base64.b64encode(cPickle.dumps(extra_data_default)) class Workflow(db.Model): - """It is a class representing a workflow.""" __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()) 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) child_logs = db.relationship("BibWorkflowEngineLog") def __repr__(self): """ Represent a workflow object.""" 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): """ Print a workflow object.""" 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): """Wrapper to get a specified object. 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): """Return the status of the workflow.""" return cls.get(Workflow.uuid == uuid).one().status @classmethod def get_most_recent(cls, *criteria, **filters): """Return 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): """Return 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): """Get the unpickle extra_data. 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: return extra_data[key] elif callable(getter): return getter(extra_data) elif not key: return extra_data def set_extra_data(self, user_id=0, uuid=None, key=None, value=None, setter=None): """Replace extra_data. 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))} ) @classmethod @session_manager def delete(cls, uuid=None): """Delete a workflow.""" cls.get(Workflow.uuid == uuid).delete() @session_manager def save(self, status): """Save object to persistent storage.""" self.modified = datetime.now() if status is not None: self.status = status db.session.add(self) class BibWorkflowObject(db.Model): - """Represent a BibWorkflowObject.""" # 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()) id_workflow = db.Column(db.String(36), db.ForeignKey("bwlWORKFLOW.uuid"), nullable=True) version = db.Column(db.Integer(3), default=ObjectVersion.INITIAL, 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) 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): """ Allow logging functionnality.""" if not self._log: db_handler_obj = BibWorkflowLogHandler(BibWorkflowObjectLog, "id") self._log = get_logger(logger_name="object.%s" % (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. :param value: value that you want to replace extra_data """ self._extra_data = base64.b64encode(cPickle.dumps(value)) def __repr__(self): """Represent a BibWorkflowObject.""" 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 __eq__(self, other): """ Enable equal operators on BibWorkflowObjects.""" 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 \ + 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): """ Enable equal operators on BibWorkflowObjects.""" return not self.__eq__(other) def add_task_result(self, name, result): """Save a task 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) if isinstance(self.extra_data["_tasks_results"], list): if not self.extra_data["_tasks_results"]: self.extra_data["_tasks_results"] = {task_name: result} else: if task_name in self.extra_data["_tasks_results"]: self.extra_data["_tasks_results"][task_name].append(res_obj) else: self.extra_data["_tasks_results"][task_name] = [res_obj] def get_tasks_results(self): """Return the complete set of tasks results.""" return self.get_extra_data()["_tasks_results"] def add_action(self, action, message): """Save an action for holdingpen for this object. Assign an special "action" to this object to be taken in consideration in holdingpen. The widget is referred to by a string with the filename minus extension. Ex: 'approval' for an action 'approval.py'. A message is also needed to tell the user the action required in a textual way. :param action: name of the action to add (i.e. "approval") :type action: string :param message: message to show to the user :type message: string """ extra_data = self.get_extra_data() extra_data["_action"] = action extra_data["_message"] = message self.set_extra_data(extra_data) def get_action(self): """Retrieve the currently assigned action, if any. :return: name of action assigned as string, or None """ try: return self.get_extra_data()["_action"] except KeyError: # No widget, try old _widget extra_data = self.get_extra_data() if "_widget" in extra_data: import warnings + warnings.warn("Widget's are now stored in '_action'", DeprecationWarning) # Migrate to new naming extra_data["_action"] = extra_data['_widget'] del extra_data["_widget"] self.set_extra_data(extra_data) return extra_data["_action"] return None def get_action_message(self): """ Retrieve the currently assigned widget, if any.""" try: return self.get_extra_data()["_message"] except KeyError: # No widget return "" def set_error_message(self, msg): """Set an error message.""" extra_data = self.get_extra_data() extra_data["_error_msg"] = msg self.set_extra_data(extra_data) def get_error_message(self): """Retrieve the error message, if any.""" if "error_msg" in self.get_extra_data(): # Backwards compatibility extra_data = self.get_extra_data() msg = extra_data["error_msg"] del extra_data["error_msg"] self.set_extra_data(extra_data) self.set_error_message(msg) try: return self.get_extra_data()["_error_msg"] except KeyError: # No message return "" def remove_action(self): """Remove the currently assigned action.""" extra_data = self.get_extra_data() extra_data["_action"] = None extra_data["_message"] = "" if "_widget" in extra_data: del extra_data["_widget"] self.set_extra_data(extra_data) def start_workflow(self, workflow_name, delayed=False, **kwargs): """Run the workflow specified on the object. Will start a new workflow execution for the object using workflows.api. :param workflow_name: name of workflow to run :type str :param delayed: should the workflow run asynchronously? :type bool """ if delayed: from .api import start_delayed as start_func else: from .api import start as start_func self.save() return start_func(workflow_name, data=[self], **kwargs) def continue_workflow(self, start_point="continue_next", delayed=False, **kwargs): """Run the workflow specified on the object. Will continue a previous execution for the object using workflows.api. :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 str :param delayed: should the workflow run asynchronously? :type bool """ from .errors import WorkflowAPIError self.save() if not self.id_workflow: raise WorkflowAPIError("No workflow associated with object: %r" % (repr(self),)) if delayed: from .api import continue_oid_delayed as continue_func else: from .api import continue_oid as continue_func return continue_func(self.id, start_point, **kwargs) def change_status(self, message): """Change the status.""" self.status = message def get_current_task(self): """Return the current task 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 save_to_file(self, directory=None, prefix="workflow_object_data_", suffix=".obj"): """Save 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): """Pickling needed function.""" return self.__dict__ def __setstate__(self, state): """ unpickling needed function.""" self.__dict__ = state def copy(self, other): """Copy 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 @session_manager def save(self, version=None, task_counter=None, id_workflow=None): """Save object to persistent storage.""" if task_counter is not None: self.log.debug("Saving task counter: %s" % (task_counter,)) extra_data = self.get_extra_data() extra_data["_task_counter"] = task_counter self.set_extra_data(extra_data) if version is not None: if version != self.version: self.modified = datetime.now() self.version = version if version in (ObjectVersion.FINAL, ObjectVersion.HALTED): redis_create_search_entry(self) if id_workflow is not None: self.id_workflow = id_workflow db.session.add(self) if self.id is not None: self.log.debug("Saving object: %s" % (self.id or "new",)) @classmethod def get(cls, *criteria, **filters): """Wrapper of Sqlalchemy to get a BibWorkflowObject. 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 @session_manager def delete(cls, oid): """Delete a BibWorkflowObject.""" cls.get(BibWorkflowObject.id == oid).delete() @classmethod @session_manager def create_object(cls, **kwargs): """Create a new Workflow Object with given content.""" obj = BibWorkflowObject(**kwargs) db.session.add(obj) return obj @classmethod @session_manager def create_object_revision(cls, old_obj, version, **kwargs): """Create a Workflow Object copy with customized values.""" # Create new object and copy it obj = BibWorkflowObject(**kwargs) obj.copy(old_obj) # Overwrite some changes obj.version = version obj.created = datetime.now() obj.modified = datetime.now() for key, value in iteritems(kwargs): setattr(obj, key, value) db.session.add(obj) return obj class BibWorkflowObjectLog(db.Model): - """Represent a BibWorkflowObjectLog. 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 __str__(self): """Print a log.""" return "%(severity)s: %(created)s - %(message)s" % { "severity": self.log_type, "created": self.created, "message": self.message } def __repr__(self): """Represent a log message.""" return "BibWorkflowObjectLog(%s)" % (", ".join([ "log_type='%s'" % self.log_type, "created='%s'" % self.created, "message='%s'" % self.message, "id_object='%s'" % self.id_object, ])) @classmethod def get(cls, *criteria, **filters): """ Sqlalchemy wrapper to get BibworkflowLogs. 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): """ Return 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): """Delete an instance in database. """ cls.get(BibWorkflowObjectLog.id == id).delete() db.session.commit() class BibWorkflowEngineLog(db.Model): - """ Represent a BibWorkflowEngineLog object.""" __tablename__ = "bwlWORKFLOWLOGGING" id = db.Column(db.Integer, primary_key=True) id_object = db.Column(db.String(255), db.ForeignKey('bwlWORKFLOW.uuid'), 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 __str__(self): """Print a log.""" return "%(severity)s: %(created)s - %(message)s" % { "severity": self.log_type, "created": self.created, "message": self.message } def __repr__(self): """Represent a log message.""" return "BibWorkflowEngineLog(%s)" % (", ".join([ "log_type='%s'" % self.log_type, "created='%s'" % self.created, "message='%s'" % self.message, "id_object='%s'" % self.id_object ])) @classmethod def get(cls, *criteria, **filters): """Sqlalchemy wrapper to get BibWorkflowEngineLog. 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): """ Return 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): """Delete an instance in database. """ cls.get(BibWorkflowEngineLog.id == uuid).delete() db.session.commit() __all__ = ['Workflow', 'BibWorkflowObject', 'BibWorkflowObjectLog', 'BibWorkflowEngineLog'] diff --git a/invenio/modules/workflows/static/js/workflows/hp_maintable.js b/invenio/modules/workflows/static/js/workflows/hp_maintable.js index 96658b4d2..61bde05fd 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_maintable.js +++ b/invenio/modules/workflows/static/js/workflows/hp_maintable.js @@ -1,112 +1,111 @@ /* * 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 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 WORKFLOWS_HOLDINGPEN = (function ($) { var oTable, oSettings, selectedRow, rowList = [], rowIndexList = [], recordsToApprove = [], defaultcss = "#example tbody tr.even:hover, #example tbody tr.odd:hover {background-color: #FFFFCC;}", context = {}, datatable = {}, tag = {}, utilities = {}, actions = []; return { oTable: oTable, oSettings: oSettings, selectedRow: selectedRow, rowList: rowList, rowIndexList: rowIndexList, recordsToApprove: recordsToApprove, defaultcss: defaultcss, context: context, datatable: datatable, tag: tag, actions: actions, utilities: utilities, init: function (data) { this.context = data; this.tag = window.WORKFLOWS_HP_TAGS; this.tag.init(); this.datatable = window.WORKFLOWS_HP_SELECTION; this.utilities = window.WORKFLOWS_HP_UTILITIES; - this.utilities.init(); this.datatable.init(this.oTable, this.oSettings); this.utilities.autorefresh(); }, init_datatable: function (datatable) { oSettings = { "dom": '<"top"iflp<"clear">>rt<"bottom"iflp<"clear">>', "bFilter": false, "bJQueryUI": true, "bProcessing": true, "bServerSide": true, "bDestroy": true, "sAjaxSource": this.context.holdingpen.url_load, "oColVis": { "buttonText": "Select Columns", "bRestore": true, "sAlign": "left", "iOverlayFade": 1 }, "aoColumnDefs": [{'bSortable': false, 'aTargets': [1]}, {'bSearchable': false, 'bVisible': false, 'aTargets': [0]}, {'sWidth': "25%", 'aTargets': [2]}, {'sWidth': "25%", 'aTargets': [3]}], "fnRowCallback": function (nRow, aData, iDisplayIndex, iDisplayIndexFull) { var id = aData[0]; datatable.rememberSelected(nRow, id); nRow.row_id = id; nRow.checkbox = nRow.cells[0].firstChild; $(nRow).on("click", "td", function (event) { console.log(event); if(event.target.nodeName != "INPUT") { datatable.selectRow(nRow, event, oTable.fnSettings()); } }); }, "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'); }); }); $('#select-all')[0].checked = false; } }; oTable = $('#maintable').dataTable(oSettings); $('.dropdown-toggle').dropdown(); this.oSettings = oTable.fnSettings(); this.oTable = oTable; } }; })(window.jQuery); diff --git a/invenio/modules/workflows/static/js/workflows/hp_tags.js b/invenio/modules/workflows/static/js/workflows/hp_tags.js index 6478a01b4..d0ec677c4 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_tags.js +++ b/invenio/modules/workflows/static/js/workflows/hp_tags.js @@ -1,112 +1,93 @@ /* * 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 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. */ // Tags functions //*********************************** var WORKFLOWS_HP_TAGS = function ($, holdingpen) { "use strict"; var tagList = []; $("#tags").tagsinput({ tagClass: function (item) { switch (item) { case 'In process': return 'label label-warning'; case 'Need action': return 'label label-danger'; case 'Done': return 'label label-success'; case 'New': return 'label label-info'; default: return 'badge badge-warning'; } } }); var init = function () { $('.task-btn').on('click', function () { if ($.inArray($(this)[0].name, tagList) <= -1) { var widget_name = $(this)[0].name; $("#tags").tagsinput('add', $(this)[0].text); WORKFLOWS_HP_UTILITIES.requestNewObjects(); } else { closeTag(widget_name); holdingpen.oTable.fnFilter('^$', 4, true, false); holdingpen.oTable.fnDraw(false); } }); $('#option-autorefresh').on('click', function () { console.log($('#option-autorefresh').hasClass("btn-danger")); if($('#option-autorefresh').hasClass("btn-danger")) { $('#option-autorefresh').removeClass("btn-danger"); } else { $('#option-autorefresh').addClass("btn-danger"); } }); $('.version-selection').on('click', function () { if ($.inArray($(this)[0].name, tagList) <= -1) { $('#tags').tagsinput('add', $(this)[0].text); } }); $("#tags").on('itemRemoved', function (event) { tagList = $("#tags").val().split(','); - tagList = taglist_translation(tagList); WORKFLOWS_HP_UTILITIES.requestNewObjects(); - holdingpen.oTable.fnDraw(false); }); $("#tags").on('itemAdded', function (event) { tagList = $("#tags").val().split(','); - tagList = taglist_translation(tagList); WORKFLOWS_HP_UTILITIES.requestNewObjects(); }); }; - function taglist_translation(my_taglist) { - var i; - for (i = 0; i <= my_taglist.length; i++) { - if (my_taglist[i] === 'Done') { - my_taglist[i] = 'Completed'; - } else if (my_taglist[i] === 'Need action') { - my_taglist[i] = 'Halted'; - } else if (my_taglist[i] === 'In process') { - my_taglist[i] = 'Running'; - } else if (my_taglist[i] === 'New') { - my_taglist[i] = 'Initial'; - } - } - return my_taglist; - } - var closeTag = function (tag_name) { tagList.splice(tagList.indexOf(tag_name), 1); $('#tags').tagsinput('remove', tag_name); }; return { init: init, tagList: function () { return tagList; }, closeTag: closeTag }; }($, WORKFLOWS_HOLDINGPEN); -//*********************************** \ No newline at end of file +//*********************************** diff --git a/invenio/modules/workflows/static/js/workflows/hp_utilities.js b/invenio/modules/workflows/static/js/workflows/hp_utilities.js index 4543e9e55..6861d691a 100644 --- a/invenio/modules/workflows/static/js/workflows/hp_utilities.js +++ b/invenio/modules/workflows/static/js/workflows/hp_utilities.js @@ -1,124 +1,77 @@ /* * 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 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. */ //Utility functions //*********************************** var WORKFLOWS_HP_UTILITIES = function ($, holdingpen) { - var tagList = holdingpen.tag.tagList, - version_showing = [holdingpen.context.version_showing]; - $.fn.exists = function () { return this.length !== 0; }; var _requestNewObjects = function () { - var version_showing = [], - i, - search_tags = []; - console.log(holdingpen); - var tempTagList = holdingpen.tag.tagList(); - for(i = 0 ; i< tempTagList.length; i++) - { - if ("Completed" == tempTagList[i]) { - version_showing.push("final"); - } else if ("Halted" == tempTagList[i]) { - version_showing.push("halted") - } else if ("Running" == tempTagList[i]) { - version_showing.push("running") - } else if ("Initial" == tempTagList[i]){ - version_showing.push("initial"); - } else { - search_tags.push(tempTagList[i]); - } - } - my_data = JSON.stringify({'version':version_showing, 'tags':search_tags}); + my_data = JSON.stringify({'tags': holdingpen.tag.tagList()}); $.ajax({ type : "POST", url : holdingpen.context.holdingpen.url_load, data: my_data, contentType: "application/json;charset=UTF-8", traditional: true, success: function(result) { holdingpen.oTable.fnDraw(false); } }); }; - var _init = function () { - var i; - if (version_showing) { - for (i = 0; i < version_showing.length; i++) { - if (version_showing[i] === 1) { - if ($.inArray('Completed', tagList) <= -1) { - tagList.push('Completed'); - } - $('#version-completed').click(); - } else if (version_showing[i] === 2) { - if ($.inArray('Halted', tagList) <= -1) { - tagList.push('Halted'); - } - $('#version-halted').click(); - } else if (version_showing[i] === 3) { - if ($.inArray("Running", tagList) <= -1) { - tagList.push("Running"); - } - $("#version-running").click(); - } - } - } - }; - var utilities = { - init: _init, requestNewObjects: _requestNewObjects, fnGetSelected: function (oTableLocal) { var aReturn = [], aTrs = oTableLocal.fnGetNodes(), i; for (i = 0; i < aTrs.length; i++) { if ($(aTrs[i]).hasClass("row_selected")) { aReturn.push(aTrs[i]); } } return aReturn; }, isInt: function (n) { return typeof n === "number" && n % 1 === 0; }, emptyLists: function () { holdingpen.rowList = []; holdingpen.rowIndexList = []; }, autorefresh: function () { window.setInterval( function() { if($('#option-autorefresh').hasClass("btn-danger")) { WORKFLOWS_HP_UTILITIES.requestNewObjects(); }}, 3000); }, }; return utilities; }($, WORKFLOWS_HOLDINGPEN); //*********************************** diff --git a/invenio/modules/workflows/templates/workflows/hp_maintable.html b/invenio/modules/workflows/templates/workflows/hp_maintable.html index a597918dd..fd2945f24 100644 --- a/invenio/modules/workflows/templates/workflows/hp_maintable.html +++ b/invenio/modules/workflows/templates/workflows/hp_maintable.html @@ -1,112 +1,112 @@ {# ## 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. #} {% extends "workflows/hp_base.html" %} {% import 'workflows/utils.html' as utils %} {% block header%} {{ super() }} {% bundles "workflows.js", "datatables.css", "datatables.js", "actions.js", "jquery.js" %} {% endblock header %} {% block javascript %} {{ super() }} <script type="text/javascript"> $(document).ready(function(){ var context_url = "{{ url_for('holdingpen.get_context')|safe }}"; window.jQuery.ajax({ url: context_url, success: function(data) { WORKFLOWS_HOLDINGPEN.init(data); WORKFLOWS_HOLDINGPEN.init_datatable(WORKFLOWS_HP_SELECTION); } }); }); </script> {% endblock javascript %} {% block hpbody %} <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 id="bs-example-navbar-collapse-1" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <!-- <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 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 action_list.items() %} <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 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">Need action</a></li> - <li><a id="version-completed" class="version-selection" name="Completed">Done</a></li> - <li><a id="version-running" class="version-selection" name="Running">In process</a></li> - <li><a id="version-initial" class="version-selection" name="Initial">New</a></li> + <li><a id="version-halted" class="version-selection" name="Nedd action">Need action</a></li> + <li><a id="version-completed" class="version-selection" name="Done">Done</a></li> + <li><a id="version-running" class="version-selection" name="In process">In process</a></li> + <li><a id="version-initial" class="version-selection" name="New">New</a></li> </ul> </li> <li class="dropdown"> <a href="#" class="dropdown-toggle dropdown-headline" data-toggle="dropdown">Options<b class="caret"></b></a> <ul class="dropdown-menu" role="menu"> <li><a id="option-autorefresh" name="autorefresh-option">Auto-refresh</a></li> </ul> </li> </ul> <ul id="navbar-right" class="nav navbar-nav navbar-right"> </ul> </div> </nav> <input class="tags" id="tags" type="text" placeholder="{{ _("Add to search") }}" value="{{tags}}"/> <br> <div class="container"> <div class="row"> <table id="maintable" cellpadding="0" cellspacing="0" border="0" class="table table-bordered"> <thead> <tr> <th>Id</th> <th><input id="select-all" type="checkbox"></th> <th>Title</th> <th>Description</th> <th>Created</th> <th>Status</th> <th>Type</th> <th>Actions</th> </tr> </thead> <tbody> </tbody> </table> </div> </div> {% endblock %} diff --git a/invenio/modules/workflows/utils.py b/invenio/modules/workflows/utils.py index efbb66197..a4563a3d9 100644 --- a/invenio/modules/workflows/utils.py +++ b/invenio/modules/workflows/utils.py @@ -1,305 +1,294 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## 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. import redis import traceback from six import iteritems from .errors import WorkflowDefinitionError def session_manager(orig_func): """Decorator to wrap function with the session.""" from invenio.ext.sqlalchemy import db def new_func(self, *a, **k): """Wrappend function to manage DB session.""" try: resp = orig_func(self, *a, **k) db.session.commit() return resp except: db.session.rollback() raise return new_func def convert_marcxml_to_bibfield(marcxml): """ :param marcxml: :return: """ from invenio.modules.jsonalchemy.reader import Reader from invenio.modules.jsonalchemy.wrappers import SmartJson if isinstance(marcxml, unicode): marcxml = marcxml.encode(errors='ignore') return Reader.translate(marcxml, SmartJson, master_format='marc', namespace='recordext') def test_teardown(self): """ Clean up created objects """ from invenio.modules.workflows.models import (BibWorkflowObject, Workflow, BibWorkflowEngineLog, BibWorkflowObjectLog) from invenio.ext.sqlalchemy import db workflows = Workflow.get(Workflow.module_name == "unit_tests").all() for workflow in workflows: BibWorkflowObject.query.filter( BibWorkflowObject.id_workflow == workflow.uuid ).delete() objects = BibWorkflowObjectLog.query.filter( BibWorkflowObject.id_workflow == workflow.uuid ).all() for obj in objects: db.session.delete(obj) db.session.delete(workflow) objects = BibWorkflowObjectLog.query.filter( BibWorkflowObject.id_workflow == workflow.uuid ).all() for obj in objects: BibWorkflowObjectLog.delete(id=obj.id) BibWorkflowEngineLog.delete(uuid=workflow.uuid) # Deleting dummy object created in tests Workflow.query.filter(Workflow.module_name == "unit_tests").delete() db.session.commit() 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 ..workflows.models import BibWorkflowObject as bwlObject if self.id is not None: return bwlObject.query.filter(bwlObject.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__} class WorkflowsTaskResult(object): """The 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 to_dict(self): return {'name': self.name,'task_name': self.task_name, 'result':self.result} def get_workflow_definition(name): """ Tries to load the given workflow from the system. """ from .registry import workflows if name in workflows: return getattr(workflows[name], "workflow", None) else: return WorkflowMissing.workflow 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): return self._fget(self._obj, key) def __setitem__(self, key, value): self._fset(self._obj, key, value) def __delitem__(self, key): self._fdel(self._obj, key) 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): - #from invenio.modules.workflows.views.holdingpen import (get_title, - # get_identifiers, - # get_subject_categories - #) should_we_reverse = False if sSortDir_0 == 'desc': should_we_reverse = True - if iSortCol_0 == 0: bwolist.sort(key=lambda x: x.id, reverse=should_we_reverse) elif iSortCol_0 == 1: bwolist.sort(key=lambda x: x.id, reverse=should_we_reverse) - #elif iSortCol_0 == 2: - # bwolist.sort(key=lambda x: get_title(x.get_data()), reverse=should_we_reverse) - #elif iSortCol_0 == 3: - # bwolist.sort(key=lambda x: get_identifiers(x.get_data()), reverse=should_we_reverse) - #elif iSortCol_0 == 4: - # bwolist.sort(key=lambda x: get_subject_categories(x.get_data()), reverse=should_we_reverse) elif iSortCol_0 == 4: bwolist.sort(key=lambda x: x.created, reverse=should_we_reverse) elif iSortCol_0 == 5: bwolist.sort(key=lambda x: x.version, reverse=should_we_reverse) elif iSortCol_0 == 6: bwolist.sort(key=lambda x: x.data_type, reverse=should_we_reverse) elif iSortCol_0 == 7: bwolist.sort(key=lambda x: x.version, reverse=should_we_reverse) elif iSortCol_0 == 8: bwolist.sort(key=lambda x: x.version, reverse=should_we_reverse) return bwolist def parse_bwids(bwolist): import ast return list(ast.literal_eval(bwolist)) def dummy_function(obj, eng): """Workflow function not found for workflow.""" pass class WorkflowMissing(object): """ Workflow is missing """ workflow = [dummy_function] class WorkflowBase(object): """Base class for workflow. Interface to define which functions should be imperatively implemented, All workflows shoud inherit from this class. """ @staticmethod def get_title(bwo, **kwargs): """Return the value to put in the title column of HoldingPen.""" return "No title" @staticmethod def get_description(bwo, **kwargs): """Return the value to put in the title column of HoldingPen.""" return "No description" @staticmethod def formatter(obj, **kwargs): raise NotImplementedError diff --git a/invenio/modules/workflows/views/holdingpen.py b/invenio/modules/workflows/views/holdingpen.py index e387e2001..1d497011c 100644 --- a/invenio/modules/workflows/views/holdingpen.py +++ b/invenio/modules/workflows/views/holdingpen.py @@ -1,650 +1,636 @@ # -*- 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 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 Holding Pen is an overlay over all objects (BibWorkflowObject) that have run through a workflow (BibWorkflowEngine). This area is targeted to catalogers and super users for inspecting ingestion workflows and submissions/depositions. Note: Currently work-in-progress. """ import re import collections from six import iteritems, text_type from flask import (render_template, Blueprint, request, jsonify, url_for, flash, 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 invenio.base.decorators import templated, wash_arguments from invenio.base.i18n import _ from invenio.utils.date import pretty_date from ..models import BibWorkflowObject, Workflow, ObjectVersion from ..registry import actions, workflows from ..utils import get_workflow_definition, sort_bwolist, WorkflowBase from ..api import continue_oid_delayed, start_delayed - blueprint = Blueprint('holdingpen', __name__, url_prefix="/admin/holdingpen", template_folder='../templates', static_folder='../static') default_breadcrumb_root(blueprint, '.holdingpen') REG_TD = re.compile("<td title=\"(.+?)\">(.+?)</td>", re.DOTALL) @blueprint.route('/', methods=['GET', 'POST']) @blueprint.route('/index', methods=['GET', 'POST']) @login_required @register_menu(blueprint, 'personalize.holdingpen', _('Your Pending Actions')) @register_breadcrumb(blueprint, '.', _('Holdingpen')) @templated('workflows/hp_index.html') def index(): """ Display main interface of Holdingpen. Acts as a hub for catalogers (may be removed) """ # FIXME: Add user filtering bwolist = get_holdingpen_objects(version_showing=[ObjectVersion.HALTED]) action_list = get_action_list(bwolist) return dict(tasks=action_list) @blueprint.route('/maintable', methods=['GET', 'POST']) @register_breadcrumb(blueprint, '.records', _('Records')) @login_required @templated('workflows/hp_maintable.html') def maintable(): """Display main table interface of Holdingpen.""" bwolist = get_holdingpen_objects() action_list = get_action_list(bwolist) my_tags = [] - if 'tags' in session: - my_tags += session['tags'] - else: - session['tags'] = my_tags - if 'workflows_version_showing' in session: - my_tags += session['workflows_version_showing'] - else: - session['workflows_version_showing'] = my_tags + if 'holdingpen_tags' in session: + my_tags = session["holdingpen_tags"] + if 'version' in request.args: - try: - if ObjectVersion.MAPPING[int(request.args.get('version'))] not in my_tags: - my_tags += [int(request.args.get('version'))] - if int(request.args.get('version')) not in session["workflows_version_showing"]: - session["workflows_version_showing"] += [int(request.args.get('version'))] - except Exception: - if request.args.get('version') not in my_tags: - my_tags += [request.args.get('version')] - if [request.args.get('version')] not in session["tags"]: - session["workflows_version_showing"] += [request.args.get('version')] + if ObjectVersion.MAPPING[int(request.args.get('version'))] not in my_tags: + my_tags += ObjectVersion.MAPPING[[int(request.args.get('version'))]] tags_to_print = "" for tag in my_tags: - try: - tags_to_print += ObjectVersion.MAPPING[tag] - except: - if tag: - tags_to_print += tag + ',' + if tag: + tags_to_print += tag + ',' return dict(bwolist=bwolist, action_list=action_list, tags=tags_to_print) -@blueprint.route('/load_table', methods=['GET', 'POST']) +@blueprint.route('/batch_action', methods=['GET', 'POST']) @login_required -@templated('workflows/hp_maintable.html') -def load_table(): - """Get JSON data for the Holdingpen table. +@wash_arguments({'bwolist': (text_type, "")}) +def batch_action(bwolist): + """Render action accepting single or multiple records.""" + from ..utils import parse_bwids + + bwolist = parse_bwids(bwolist) - 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. - """ - workflows_cache = {} - version_showing = [] - req = None - req_search = [] - if request.json: - if "version" in request.json: - req = request.json["version"] - if "tags" in request.json: - req_search = request.json["tags"] - session["tags"] = req_search - if req is not None: - if "final" in req: - version_showing.append(ObjectVersion.FINAL) - if "halted" in req: - version_showing.append(ObjectVersion.HALTED) - if "running" in req: - version_showing.append(ObjectVersion.RUNNING) - if "initial" in req: - version_showing.append(ObjectVersion.INITIAL) - session['workflows_version_showing'] = version_showing - if req == []: - version_showing = ObjectVersion.MAPPING.keys() - elif 'workflows_version_showing' in session: - version_showing = session.get('workflows_version_showing', []) - 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 = session.get('iSortCol_0', 0) - s_sortdir_0 = session.get('sSortDir_0', None) - i_display_start = session.get('iDisplayLength', 10) - i_display_length = session.get('iDisplayLength', 0) - sEcho = session.get('sEcho', 0) + 1 - - s_search = request.args.get('sSearch', None) - if s_search: - if req_search: - s_search = s_search.split(',') + req_search - else: - s_search = s_search.split(',') + session["tags"] - elif req_search: - s_search = req_search - - if s_search is [u'']: - s_search = [] - bwolist = get_holdingpen_objects(ssearch=s_search, - version_showing=version_showing) - if 'iSortCol_0' in session: - i_sortcol_0 = int(i_sortcol_0) - if i_sortcol_0 != session['iSortCol_0'] \ - or s_sortdir_0 != session['sSortDir_0']: - bwolist = sort_bwolist(bwolist, i_sortcol_0, s_sortdir_0) - session['iDisplayStart'] = i_display_start - session['iDisplayLength'] = i_display_length - session['iSortCol_0'] = i_sortcol_0 - session['sSortDir_0'] = s_sortdir_0 - session['sEcho'] = sEcho - - table_data = { - "aaData": [] - } try: - table_data['iTotalRecords'] = len(bwolist) - table_data['iTotalDisplayRecords'] = len(bwolist) - except TypeError: - bwolist = get_holdingpen_objects(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]: - records_showing += 1 + bwolist = map(int, bwolist) + except ValueError: + # Bad ID, we just pass for now + pass - action_name = bwo.get_action() - action_message = bwo.get_action_message() - if not action_message: - action_message = "" - - action = actions.get(action_name, None) - - workflows_uuid = bwo.id_workflow - if workflows_uuid and workflows_uuid not in workflows_cache: - workflows_cache[workflows_uuid] = workflows[Workflow.query.filter(Workflow.uuid == workflows_uuid).one().name]() - - mini_action = None - if action: - mini_action = getattr(action, "render_mini", None) - - record = bwo.get_data() - if not hasattr(record, "get"): - try: - record = dict(record) - except: - record = {} - - if not workflows_uuid: - title = "No title" - description = "No description" - extra_data = bwo.get_extra_data() - else: - title = workflows_cache[workflows_uuid].get_title(bwo) - description = workflows_cache[workflows_uuid].get_description(bwo) - extra_data = bwo.get_extra_data() - - row = render_template('workflows/row_formatter.html', - title=title, - object=bwo, - record=record, - extra_data=extra_data, - description=description, - action=action, - mini_action=mini_action, - action_message=action_message, - pretty_date=pretty_date, - ) - d = {} - for key, value in REG_TD.findall(row): - d[key] = value.strip() - - table_data['aaData'].append( - [d['id'], - d['checkbox'], - d['title'], - d['description'], - d['pretty_date'], - d['version'], - d['type'], - d['action']] - ) - table_data['sEcho'] = sEcho - table_data['iTotalRecords'] = len(bwolist) - table_data['iTotalDisplayRecords'] = len(bwolist) - return jsonify(table_data) + objlist = [] + workflow_func_list = [] + w_metadata_list = [] + info_list = [] + actionlist = [] + 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_action() not in actionlist: + actionlist.append(bwobject.get_action()) + + action_form = actions[actionlist[0]] + + result = action_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('/get_version_showing', methods=['GET', 'POST']) @login_required def get_version_showing(): """Return current version showing, from flask session.""" try: return session['workflows_version_showing'] except KeyError: return None @blueprint.route('/details/<int:objectid>', methods=['GET', 'POST']) @register_breadcrumb(blueprint, '.details', _("Record Details")) @login_required def details(objectid): """Display info about the object.""" of = "hd" bwobject = BibWorkflowObject.query.get(objectid) from invenio.ext.sqlalchemy import db #formatted_data = bwobject.get_formatted_data(of) workflows_uuid = bwobject.id_workflow if workflows_uuid: - formatted_data = workflows[Workflow.query.filter(Workflow.uuid == workflows_uuid).one().name]().formatter(bwobject, formatter=None, format=of) + formatted_data = workflows[Workflow.query.get(workflows_uuid).name]().formatter(bwobject, formatter=None, format=of) else: formatted_data = "" extracted_data = extract_data(bwobject) action_name = bwobject.get_action() if action_name: action = actions[action_name] rendered_actions = action().render(bwobject) else: rendered_actions = {} if bwobject.id_parent: hbwobject_db_request = BibWorkflowObject.query.filter( db.or_(BibWorkflowObject.id_parent == bwobject.id_parent, BibWorkflowObject.id == bwobject.id_parent, BibWorkflowObject.id == bwobject.id)).all() else: hbwobject_db_request = BibWorkflowObject.query.filter( db.or_(BibWorkflowObject.id_parent == bwobject.id, BibWorkflowObject.id == bwobject.id)).all() hbwobject = {ObjectVersion.FINAL: [], ObjectVersion.HALTED: [], ObjectVersion.INITIAL: [], ObjectVersion.RUNNING: []} for hbobject in hbwobject_db_request: hbwobject[hbobject.version].append({"id": hbobject.id, "version": hbobject.version, "date": pretty_date(hbobject.created), "true_date": hbobject.modified}) for list_of_object in hbwobject: hbwobject[list_of_object].sort(key=lambda x: x["true_date"], reverse=True) hbwobject_final = hbwobject[ObjectVersion.INITIAL] + \ hbwobject[ObjectVersion.HALTED] + \ hbwobject[ObjectVersion.FINAL] results = [] for label, res in bwobject.get_tasks_results().iteritems(): res_dicts = [item.to_dict() for item in res] results.append((label, res_dicts)) return render_template('workflows/hp_details.html', bwobject=bwobject, rendered_actions=rendered_actions, hbwobject=hbwobject_final, bwparent=extracted_data['bwparent'], info=extracted_data['info'], log=extracted_data['logtext'], data_preview=formatted_data, workflow_func=extracted_data['workflow_func'], workflow=extracted_data['w_metadata'], task_results=results) @blueprint.route('/restart_record', methods=['GET', 'POST']) @login_required @wash_arguments({'objectid': (int, 0)}) def restart_record(objectid, start_point='continue_next'): """Restart the initial object in its workflow.""" bwobject = BibWorkflowObject.query.get(objectid) workflow = Workflow.query.filter( Workflow.uuid == bwobject.id_workflow).first() start_delayed(workflow.name, [bwobject.get_data()]) return 'Record Restarted' @blueprint.route('/continue_record', methods=['GET', 'POST']) @login_required @wash_arguments({'objectid': (int, 0)}) def continue_record(objectid): """Continue workflow for current object.""" 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({'objectid': (int, 0)}) def restart_record_prev(objectid): """Restart the last task for current object.""" continue_oid_delayed(oid=objectid, start_point="restart_task") return 'Record restarted current task' @blueprint.route('/delete', methods=['GET', 'POST']) @login_required @wash_arguments({'objectid': (int, 0)}) def delete_from_db(objectid): """Delete the object from the db.""" BibWorkflowObject.delete(objectid) return 'Record Deleted' @blueprint.route('/delete_multi', methods=['GET', 'POST']) @login_required @wash_arguments({'bwolist': (text_type, "")}) def delete_multi(bwolist): """Delete list of objects from the db.""" from ..utils import parse_bwids bwolist = parse_bwids(bwolist) for objectid in bwolist: delete_from_db(objectid) return 'Records Deleted' @blueprint.route('/resolve', methods=['GET', 'POST']) @login_required @wash_arguments({'objectid': (text_type, '-1')}) def resolve_action(objectid): """Resolve the action taken. Will call the run() function of the specific action. """ bwobject = BibWorkflowObject.query.get(int(objectid)) action_name = bwobject.get_action() action_form = actions[action_name] res = action_form().resolve(bwobject) return jsonify(res) @blueprint.route('/entry_data_preview', methods=['GET', 'POST']) @login_required @wash_arguments({'objectid': (text_type, '0'), 'of': (text_type, None)}) def entry_data_preview(objectid, of): """Present the data in a human readble form or in xml code.""" from flask import Markup from pprint import pformat bwobject = BibWorkflowObject.query.get(int(objectid)) if not bwobject: flash("No object found for %s" % (objectid,)) return jsonify(data={}) workflows_uuid = bwobject.id_workflow if workflows_uuid: - formatted_data = workflows[Workflow.query.filter(Workflow.uuid == workflows_uuid).one().name]().formatter(bwobject, formatter=None, format=of) + formatted_data = workflows[Workflow.query.get(workflows_uuid).name]().formatter(bwobject, formatter=None, format=of) else: formatted_data = "" if isinstance(formatted_data, dict): formatted_data = pformat(formatted_data) if of and of in ("xm", "xml", "marcxml"): data = Markup.escape(formatted_data) else: data = formatted_data return jsonify(data=data) @blueprint.route('/get_context', methods=['GET', 'POST']) @login_required def get_context(): """Return the a JSON structure with URL maps and actions.""" context = {} context['url_prefix'] = blueprint.url_prefix context['holdingpen'] = { "url_load": url_for('holdingpen.load_table'), "url_preview": url_for('holdingpen.entry_data_preview'), "url_restart_record": url_for('holdingpen.restart_record'), "url_restart_record_prev": url_for('holdingpen.restart_record_prev'), "url_continue_record": url_for('holdingpen.continue_record'), } try: context['version_showing'] = session['workflows_version_showing'] except KeyError: context['version_showing'] = ObjectVersion.HALTED return jsonify(context) 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['action'] = bwobject.get_action() return info def extract_data(bwobject): """Extract needed metadata from BibWorkflowObject. Used for rendering the Record's holdingpen table row and details and action 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']['action'] = bwobject.get_action() except (KeyError, AttributeError): pass extracted_data['w_metadata'] = \ Workflow.query.filter(Workflow.uuid == bwobject.id_workflow).first() if extracted_data['w_metadata']: workflow_def = get_workflow_definition(extracted_data['w_metadata'].name) extracted_data['workflow_func'] = workflow_def else: extracted_data['workflow_func'] = [None] return extracted_data def get_action_list(object_list): """Return a dict of action names mapped to halted objects. Get a dictionary mapping from action name to number of Pending actions (i.e. halted objects). Used in the holdingpen.index page. """ action_dict = {} found_actions = [] # First get a list of all to count up later for bwo in object_list: action_name = bwo.get_action() if action_name is not None: found_actions.append(action_name) # Get "real" action name only once per action for action_name in set(found_actions): if action_name not in actions: # Perhaps some old action? Use stored name. action_nicename = action_name else: action = actions[action_name] action_nicename = getattr(action, "name", action_name) action_dict[action_nicename] = found_actions.count(action_name) return action_dict -def get_holdingpen_objects(isortcol_0=None, - ssortdir_0=None, - ssearch=None, - version_showing=(ObjectVersion.HALTED,)): +@blueprint.route('/load_table', methods=['GET', 'POST']) +@login_required +@templated('workflows/hp_maintable.html') +def load_table(): + """Get JSON data for the Holdingpen table. + + 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. + """ + import time + StartA = time.time() + try: + workflows_cache = {} + if request.method == "POST": + if "holdingpen_tags" not in session: + session["holdingpen_tags"] = [] + if request.json and "tags" in request.json: + tags = request.json["tags"] + session["holdingpen_tags"] = tags + elif "holdingpen_tags" in session: + tags = session["holdingpen_tags"] + else: + tags = [] + session["holdingpen_tags"] = [] + print "global request {0}".format(time.time() - StartA) + return None + else: + if "holdingpen_tags" in session: + tags = session["holdingpen_tags"] + else: + tags = [] + session["holdingpen_tags"] = [] + + i_sortcol_0 = request.args.get('iSortCol_0', session.get('iSortCol_0', 0)) + s_sortdir_0 = request.args.get('sSortDir_0', session.get('sSortDir_0', None)) + + session["holdingpen_iDisplayStart"] = int(request.args.get('iDisplayStart', session.get('iDisplayLength', 10))) + session["holdingpen_iDisplayLength"] = int(request.args.get('iDisplayLength', session.get('iDisplayLength', 0))) + session["holdingpen_sEcho"] = int(request.args.get('sEcho', session.get('sEcho', 0))) + 1 + bwolist = get_holdingpen_objects(tags) + print "get holdingpen object {0}".format(time.time() - StartA) + if 'iSortCol_0' in session and "sSortDir_0" in session: + i_sortcol_0 = int(str(i_sortcol_0)) + if i_sortcol_0 != session['iSortCol_0'] or s_sortdir_0 != session['sSortDir_0']: + bwolist = sort_bwolist(bwolist, i_sortcol_0, s_sortdir_0) + session["holdingpen_iSortCol_0"] = i_sortcol_0 + session["holdingpen_sSortDir_0"] = s_sortdir_0 + + table_data = {'aaData': [], + 'iTotalRecords': len(bwolist), + 'iTotalDisplayRecords': len(bwolist), + 'sEcho': session["holdingpen_sEcho"]} + + # This will be simplified once Redis is utilized. + records_showing = 0 + for bwo in bwolist[session["holdingpen_iDisplayStart"]:session["holdingpen_iDisplayStart"] + session["holdingpen_iDisplayLength"]]: + action_name = bwo.get_action() + action_message = bwo.get_action_message() + if not action_message: + action_message = "" + + action = actions.get(action_name, None) + records_showing += 1 + workflows_uuid = bwo.id_workflow + if workflows_uuid and workflows_uuid not in workflows_cache: + workflows_cache[workflows_uuid] = workflows[Workflow.query.get(workflows_uuid).name]() + mini_action = getattr(action, "mini_action", None) + record = bwo.get_data() + if not hasattr(record, "get"): + try: + record = dict(record) + except: + record = {} + if not workflows_uuid: + title = "No title" + description = "No description" + extra_data = bwo.get_extra_data() + else: + title = workflows_cache[workflows_uuid].get_title(bwo) + description = workflows_cache[workflows_uuid].get_description(bwo) + extra_data = bwo.get_extra_data() + row = render_template('workflows/row_formatter.html', + title=title, + object=bwo, + record=record, + extra_data=extra_data, + description=description, + action=action, + mini_action=mini_action, + action_message=action_message, + pretty_date=pretty_date, + ) + d = {} + for key, value in REG_TD.findall(row): + d[key] = value.strip() + + table_data['aaData'].append( + [d['id'], + d['checkbox'], + d['title'], + d['description'], + d['pretty_date'], + d['version'], + d['type'], + d['action']] + ) + print "global request {0}".format(time.time() - StartA) + return jsonify(table_data) + except Exception as e: + print e + + +def get_holdingpen_objects(ptags=[]): """Get BibWorkflowObject's for display in Holding Pen. Uses DataTable naming for filtering/sorting. Work in progress. """ + import time workflows_cache = {} - - if isortcol_0: - isortcol_0 = int(isortcol_0) + tags = ptags[:] + temp_list = ["New", "Done", "Need action", "In process"] + version_showing = [] + for i in range(len(tags) - 1, -1, -1): + if tags[i] in temp_list: + version_showing.append(ObjectVersion.REVERSE_MAPPING[tags[i]]) + del tags[i] + + if version_showing is None: + version_showing = ObjectVersion.MAPPING.keys() + ssearch = tags bwobject_list = BibWorkflowObject.query.filter( BibWorkflowObject.id_parent == None ).filter(not version_showing or BibWorkflowObject.version.in_( version_showing)).all() + if ssearch: + if not isinstance(ssearch, list): + if "," in ssearch: + ssearch = ssearch.split(",") + else: + ssearch = [ssearch] + bwobject_list_tmp = [] for bwo in bwobject_list: - extra_data = bwo.get_extra_data() - data = bwo.get_data() - if not isinstance(data, collections.Mapping): - data = {} + startA = time.time() workflows_uuid = bwo.id_workflow if workflows_uuid and workflows_uuid not in workflows_cache: - workflows_cache[workflows_uuid] = workflows[Workflow.query.filter(Workflow.uuid == workflows_uuid).one().name]() - all_parameters = {"record": data, "extra_data": extra_data, - "bwo": bwo} - if not isinstance(ssearch, list): - if "," in ssearch: - ssearch = ssearch.split(",") - else: - ssearch = [ssearch] + workflows_cache[workflows_uuid] = workflows[Workflow.query.get(workflows_uuid).name]() + if workflows_uuid: checking_functions = {"title": workflows_cache[workflows_uuid].get_title, "description": workflows_cache[workflows_uuid].get_description, "created": get_pretty_date, "type": get_type } else: checking_functions = {"title": WorkflowBase.get_title, "description": WorkflowBase.get_description, "created": get_pretty_date, "type": get_type } - confirm = 0 - to_add = True - - for term in ssearch: - for function in checking_functions: - function = checking_functions[function] - func_code = function.func_code - if check_ssearch_over_data(term, function( - **dict((func_code.co_varnames[i], - all_parameters[func_code.co_varnames[i]]) - for i in range(0, func_code.co_argcount)))): - confirm += 1 - if confirm == 0: - to_add = False - confirm = 0 - - if to_add: - bwobject_list_tmp.append(bwo) + startB = time.time() + for function in checking_functions: + if check_ssearch_over_data(ssearch, checking_functions[function](bwo)): + bwobject_list_tmp.append(bwo) + break + else: + print "A {0}, B {1}, ratio {2}".format(time.time() - startA,time.time() - startB,(time.time() - startB)/(time.time() - startA)) + break # executed if the loop ended normally (no break) + print "A {0}, B {1}, ratio {2}".format(time.time() - startA,time.time() - startB,(time.time() - startB)/(time.time() - startA)) + continue + bwobject_list = bwobject_list_tmp - if isortcol_0 == -6: - if ssortdir_0 == 'desc': - bwobject_list.reverse() return bwobject_list def check_ssearch_over_data(ssearch, data): """ Check if the data match with one of the search tag in data. :param ssearch: list of tags used for filtering. :param data: data to check. :return: True if present False otherwise. """ if not isinstance(ssearch, list): if "," in ssearch: ssearch = ssearch.split(",") else: ssearch = [ssearch] if not isinstance(data, list): data = [data] - count = 0 + + result = True + for terms in ssearch: for datum in data: - if terms.lower() in datum.lower(): - count += 1 - if count > 0: - return True - else: - return False + if terms.lower() not in datum.lower(): + result = False + break + return result def get_pretty_date(bwo): return pretty_date(bwo.created) def get_type(bwo): """Get the type of the Object.""" return bwo.data_type