Backport import ui from saem_ref
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 20 Oct 2016 19:08:05 +0200
changeset 1908 79c8835a856b
parent 1907 3eaf32ca3e95
child 1909 f060b5932206
Backport import ui from saem_ref At some point, we'll probably have to: * ensure action doesn't appear in case it shouldn't (eg reference archive units) * move it to the archive unit tab rather than in the actions box But this is a almost direct backport, enhancements will come later.
data/cubes.editionext.js
test/test_views.py
views/archivetransfer.py
views/archiveunit.py
views/clone.py
views/widgets.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/cubes.editionext.js	Thu Oct 20 19:08:05 2016 +0200
@@ -0,0 +1,35 @@
+// copyright 2015-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+// contact http://www.logilab.fr -- mailto:contact@logilab.fr
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation, either version 2.1 of the License, or (at your option)
+// any later version.
+//
+// This program 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 Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>.
+
+editext = {
+    relateWidget: function(domid, search_url, title, multiple, onValidate) {
+        options = {'dialogOptions': {'title': title},
+                   'editOptions': {'required': true,
+                                   'multiple': multiple,
+                                   'searchurl': search_url}
+                  };
+        options.onValidate = onValidate;
+        cw.jqNode(domid).relationwidget(options);
+    },
+
+    buildSedaImportValidate: function(eid) {
+        var validate = function(selected) {
+            const cloned = Object.keys(selected).join(',');
+            document.location = BASE_URL + 'seda.doimport?eid=' + eid + '&cloned=' + cloned;
+        };
+        return validate;
+    }
+}
--- a/test/test_views.py	Thu Oct 20 19:06:14 2016 +0200
+++ b/test/test_views.py	Thu Oct 20 19:08:05 2016 +0200
@@ -23,9 +23,9 @@
 from cubicweb.web import INTERNAL_FIELD_VALUE
 
 from cubes.seda.xsd2yams import RULE_TYPES
-from cubes.seda.views import export
+from cubes.seda.views import export, clone
 
-from testutils import create_transfer_to_bdo, create_archive_unit
+from testutils import create_transfer_to_bdo, create_archive_unit, create_data_object
 
 
 class ManagementRulesTC(CubicWebTC):
@@ -362,5 +362,110 @@
                 self.assertEqual(form.form_renderer_id, 'not-an-alt')
 
 
+class CloneActionsTC(CubicWebTC):
+
+    def test_actions_notype_to_import(self):
+
+        with self.admin_access.web_request() as req:
+            transfer = req.create_entity('SEDAArchiveTransfer', title=u'test')
+            unit = create_archive_unit(transfer)[0]
+            req.cnx.commit()
+            actions = self.pactionsdict(req, transfer.as_rset())
+            self.assertIn(clone.ImportSEDAArchiveUnitAction, actions['moreactions'])
+            actions = self.pactionsdict(req, unit.as_rset())
+            self.assertIn(clone.ImportSEDAArchiveUnitAction, actions['moreactions'])
+        with self.new_access('anon').web_request() as req:
+            profile = req.entity_from_eid(transfer.eid)
+            actions = self.pactionsdict(req, profile.as_rset())
+            self.assertNotIn(clone.ImportSEDAArchiveUnitAction, actions['moreactions'])
+
+
+class CloneImportTC(CubicWebTC):
+    """Tests for 'seda.doimport' controller (called from JavaScript)."""
+
+    def setup_database(self):
+        with self.admin_access.cnx() as cnx:
+            # scheme = testutils.seda_scheme(
+            #     cnx, u'seda_document_type_code', u'preferred', u'QWE')
+            # self.doctypecodevalue_eid = scheme.reverse_in_scheme[0].eid
+            # doctypecode = self.doctypecode(cnx)
+            # self.doc_eid = cnx.create_entity(
+            #     'ProfileDocument', user_cardinality=u'1',
+            #     user_annotation=u'plop',
+            #     seda_document_type_code=doctypecode).eid
+            transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test')
+            unit, unit_alt, unit_alt_seq = create_archive_unit(None, cnx=cnx,
+                                                               user_cardinality=u'1',
+                                                               user_annotation=u'plop')
+            bdo = create_data_object(None, cnx=cnx, filename=u'file.txt')
+            cnx.create_entity('SEDADataObjectReference', user_cardinality=u'0..n',
+                              seda_data_object_reference=unit_alt_seq,
+                              seda_data_object_reference_id=bdo)
+            self.transfer_eid = transfer.eid
+            self.unit_eid = unit.eid
+            self.bdo_eid = bdo.eid
+
+            # self.archobj_eid = cnx.create_entity(
+            #     'ProfileArchiveObject',
+            #     seda_name=cnx.create_entity('SEDAName'),
+            #     seda_parent=profile).eid
+            cnx.commit()
+
+    def doctypecode(self, cnx):
+        return cnx.create_entity(
+            'SEDADocumentTypeCode',
+            seda_document_type_code_value=self.doctypecodevalue_eid)
+
+    def test_import_one_entity(self):
+        params = dict(eid=text_type(self.transfer_eid),
+                      cloned=text_type(self.unit_eid))
+        with self.admin_access.web_request(**params) as req:
+            path, _ = self.expect_redirect_handle_request(
+                req, 'seda.doimport')
+            etype, eid = path.split('/')
+            self.assertEqual(etype, 'SEDAArchiveTransfer'.lower())
+            clone = req.execute('Any X WHERE X seda_archive_unit P, P eid %(p)s',
+                                {'p': eid}).one()
+            self.assertEqual([x.eid for x in clone.clone_of], [self.unit_eid])
+            # Check that original entity attributes have been copied.
+            self.assertEqual(clone.user_cardinality, u'1')
+            self.assertEqual(clone.user_annotation, u'plop')
+            # Check that data object has been copied and linked to the transfer
+            seq = clone.first_level_choice.content_sequence
+            bdo = seq.reverse_seda_data_object_reference[0].seda_data_object_reference_id[0]
+            self.assertNotEqual(bdo.eid, self.bdo_eid)
+            self.assertEqual(bdo.filename, 'file.txt')
+            transfer = req.entity_from_eid(self.transfer_eid)
+            transfer_bdos = [do.eid for do in transfer.reverse_seda_binary_data_object]
+            self.assertEqual(transfer_bdos, [bdo.eid])
+
+    def test_import_multiple_entities(self):
+        with self.admin_access.cnx() as cnx:
+            unit, unit_alt, unit_alt_seq = create_archive_unit(None, cnx=cnx,
+                                                               id=u'new',
+                                                               user_cardinality=u'0..1',
+                                                               user_annotation=u'plouf')
+            cnx.commit()
+        to_clone = [self.unit_eid, unit.eid]
+        params = dict(eid=text_type(self.transfer_eid),
+                      cloned=','.join([text_type(self.unit_eid), text_type(unit.eid)]))
+        with self.admin_access.web_request(**params) as req:
+            path, _ = self.expect_redirect_handle_request(
+                req, 'seda.doimport')
+            etype, eid = path.split('/')
+            self.assertEqual(etype, 'SEDAArchiveTransfer'.lower())
+            rset = req.execute(
+                'Any X,O WHERE X seda_archive_unit P, P eid %(p)s, X clone_of O',
+                {'p': eid})
+            self.assertEqual(len(rset), 2)
+            self.assertCountEqual([oeid for __, oeid in rset.rows], to_clone)
+            cardinalities, annotations = zip(*[
+                (clone.user_cardinality, clone.user_annotation)
+                for clone in rset.entities()])
+            self.assertCountEqual(cardinalities, ('1', '0..1'))
+            self.assertCountEqual(annotations, ('plop', 'plouf'))
+
+
+
 if __name__ == '__main__':
     unittest.main()
--- a/views/archivetransfer.py	Thu Oct 20 19:06:14 2016 +0200
+++ b/views/archivetransfer.py	Thu Oct 20 19:08:05 2016 +0200
@@ -23,10 +23,12 @@
 from cubicweb.web import formwidgets as fw
 from cubicweb.web.views import tabs, uicfg, reledit
 
+from cubes.relationwidget import views as rwdg
+
 from cubes.seda.xsd2yams import RULE_TYPES
 from cubes.seda.entities import full_seda2_profile, simplified_profile, parent_and_container
 from cubes.seda.views import rtags_from_xsd_element, rtags_from_rtype_role_targets
-from cubes.seda.views import viewlib, copy_rtag
+from cubes.seda.views import viewlib, clone, copy_rtag
 from cubes.seda.views import uicfg as sedauicfg  # noqa - ensure those rules are defined first
 
 at_ordered_fields = [
@@ -113,6 +115,10 @@
         _('seda_at_diagnose_tab'),
     ]
 
+    def entity_call(self, entity, **kwargs):
+        super(ArchiveTransferTabbedPrimaryView, self).entity_call(entity, **kwargs)
+        rwdg.boostrap_dialog(self.w, self._cw._, clone._import_div_id(entity), u'')
+
 
 class ArchiveTransferCodeListVersionsTab(tabs.PrimaryTab):
     """Tab for code list versions information of an archive transfer."""
--- a/views/archiveunit.py	Thu Oct 20 19:06:14 2016 +0200
+++ b/views/archiveunit.py	Thu Oct 20 19:08:05 2016 +0200
@@ -25,12 +25,13 @@
 from cubicweb.web.views import uicfg, baseviews, tabs
 
 from cubes.compound import views as compound
+from cubes.relationwidget import views as rwdg
 
 from cubes.seda.entities import simplified_profile, parent_and_container
 from cubes.seda.entities.itree import parent_archive_unit
 from cubes.seda.views import (add_subobject_link, add_subobjects_button, dropdown_button,
                               rtags_from_rtype_role_targets, copy_rtag)
-from cubes.seda.views import viewlib, widgets, content
+from cubes.seda.views import clone, viewlib, widgets, content
 from cubes.seda.views import uicfg as sedauicfg  # noqa - ensure those rules are defined first
 
 
@@ -146,6 +147,11 @@
         _('seda_au_data_objects_refs_tab'),
     ]
 
+    def entity_call(self, entity, **kwargs):
+        super(ArchiveUnitTabbedPrimaryView, self).entity_call(entity, **kwargs)
+        rwdg.boostrap_dialog(self.w, self._cw._, clone._import_div_id(entity), u'')
+
+
 au_ref_pvs = copy_rtag(pvs, __name__,
                        is_instance('SEDAArchiveUnit') & is_archive_unit_ref())
 au_ref_pvs.tag_subject_of(
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/clone.py	Thu Oct 20 19:08:05 2016 +0200
@@ -0,0 +1,126 @@
+# copyright 2015-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact@logilab.fr
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program 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 Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+"""Views related to cloning of SEDA compound tree."""
+
+from cubicweb import uilib, _
+from cubicweb.predicates import match_form_params, has_permission, one_line_rset, is_instance
+from cubicweb.web import Redirect, action, controller
+from cubicweb.web.views import actions, uicfg
+
+from cubes.compound.entities import copy_entity
+from cubes.relationwidget import views as rwdg
+
+from cubes.seda.views.widgets import configure_relation_widget
+
+
+# Hide copy action for SEDA profiles
+actions.CopyAction.__select__ = actions.CopyAction.__select__ & ~is_instance('SEDAArchiveTransfer')
+
+
+afs = uicfg.autoform_section
+pvs = uicfg.primaryview_section
+
+# clone relation
+afs.tag_subject_of(('*', 'clone_of', '*'), 'main', 'hidden')
+afs.tag_object_of(('*', 'clone_of', '*'), 'main', 'hidden')
+
+
+class ImportAction(action.Action):
+    __abstract__ = True
+
+    __select__ = (action.Action.__select__
+                  & one_line_rset()
+                  & has_permission('update'))
+
+    category = 'moreactions'
+    submenu = _('import menu')
+    etype = None  # to be specified in concrete classes
+
+    @property
+    def title(self):
+        return self._cw._(self.etype).lower()
+
+    def fill_menu(self, box, menu):
+        # when there is only one item in the sub-menu, replace the sub-menu by
+        # item's title prefixed by 'add'
+        menu.label_prefix = self._cw._('import menu')
+        super(ImportAction, self).fill_menu(box, menu)
+
+    def url(self):
+        self._cw.add_js('cubes.editionext.js')
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        if entity.cw_etype == 'SEDAArchiveTransfer':
+            root = entity
+        else:
+            root = entity.cw_adapt_to('IContained').container or entity
+        relation = 'clone_of:%s:subject' % self.etype
+        search_url = self._cw.build_url('ajax', fname='view', vid='search_related_entities',
+                                        __modal=1, multiple='1', relation=relation,
+                                        etype=root.cw_etype, target=root.eid)
+        title = self._cw._('Search entity to import')
+        return configure_relation_widget(self._cw, _import_div_id(entity), search_url, title,
+                                         True, uilib.js.editext.buildSedaImportValidate(entity.eid))
+
+
+class ImportSEDAArchiveUnitAction(ImportAction):
+    __regid__ = 'seda.import.archiveunit'
+    __select__ = ImportAction.__select__ & is_instance('SEDAArchiveTransfer', 'SEDAArchiveUnit')
+    etype = 'SEDAArchiveUnit'
+
+
+class DoImportView(controller.Controller):
+    __regid__ = 'seda.doimport'
+    __select__ = match_form_params('eid', 'cloned')
+
+    def publish(self, rset=None):
+        ui_parent = self._cw.entity_from_eid(int(self._cw.form['eid']))
+        if ui_parent.cw_etype == 'SEDAArchiveTransfer':
+            parent = ui_parent
+        else:
+            parent = ui_parent.first_level_choice.content_sequence
+        clones = []
+        for cloned_eid in self._cw.form['cloned'].split(','):
+            original = self._cw.entity_from_eid(int(cloned_eid))
+            clones.append(copy_entity(original, seda_archive_unit=parent,
+                                      clone_of=original))
+        basemsg = (_('{0} has been imported') if len(clones) == 1 else
+                   _('{0} have been imported'))
+        msg = self._cw._(basemsg).format(u', '.join(clone.dc_title() for clone in clones))
+        raise Redirect(ui_parent.absolute_url(__message=msg))
+
+
+def _import_div_id(entity):
+    """Return identifier of div place holder for the relation widget.
+
+    You've to put this on entity's primary view for import action to work.
+    """
+    return 'importDiv%s' % entity.eid
+
+
+class SearchForTargetToImportView(rwdg.SearchForRelatedEntitiesView):
+    __select__ = (rwdg.SearchForRelatedEntitiesView.__select__
+                  & rwdg.edited_relation('clone_of')
+                  & match_form_params('target'))
+    title = None
+    has_creation_form = False
+
+    def linkable_rset(self):
+        """Return rset of entities to be displayed as possible values for the edited relation."""
+        tetype = self._cw.form['relation'].split(':')[1]
+        target = int(self._cw.form['target'])
+        rql = ('Any X,MD ORDERBY MD DESC WHERE X is %s, X modification_date MD, '
+               'NOT X seda_archive_unit P, NOT X eid %%(target)s') % tetype
+        return self._cw.execute(rql, {'target': target})
--- a/views/widgets.py	Thu Oct 20 19:06:14 2016 +0200
+++ b/views/widgets.py	Thu Oct 20 19:08:05 2016 +0200
@@ -25,6 +25,24 @@
 from cubicweb.web.views import ajaxcontroller, formrenderers, autoform
 
 
+def configure_relation_widget(req, div, search_url, title, multiple, validate):
+    """Build a javascript link to invoke a relation widget
+
+    Widget will be linked to div `div`, with a title `title`. It will display selectable entities
+    matching url `search_url`. bool `multiple` indicates whether several entities can be selected or
+    just one, `validate` identifies the javascript callback that must be used to validate the
+    selection.
+    """
+    req.add_js(('jquery.ui.js',
+                'cubicweb.ajax.js',
+                'cubicweb.widgets.js',
+                'cubicweb.facets.js',
+                'cubes.relationwidget.js',
+                'cubes.editionext.js'))
+    return 'javascript: %s' % js.editext.relateWidget(div, search_url, title, multiple,
+                                                      utils.JSString(validate))
+
+
 class ConceptOrTextField(ff.Field):
     """Compound field, using the :class:`ConceptAutoCompleteWidget` to handle values which may be
     either a relation to a `Concept` or a string