# HG changeset patch # User Sylvain Thénault # Date 1253019701 -7200 # Node ID 1d25e928c2997bf0e8482bdab177f09eb5c23be7 # Parent fc63b80ec97931d24731cc1a242d71860cb63c4f# Parent 948e0cb59b1aa89d8607b6113f42315627796980 backport 3.5 diff -r fc63b80ec979 -r 1d25e928c299 common/i18n.py --- a/common/i18n.py Mon Sep 14 11:25:56 2009 +0200 +++ b/common/i18n.py Tue Sep 15 15:01:41 2009 +0200 @@ -44,12 +44,9 @@ """display the command, execute it and raise an Exception if returned status != 0 """ + from subprocess import call print cmd.replace(os.getcwd() + os.sep, '') - if sys.platform == 'win32': - from subprocess import call - else: - call = os.system - status = call(cmd) + status = call(cmd, shell=True) if status != 0: raise Exception('status = %s' % status) diff -r fc63b80ec979 -r 1d25e928c299 common/migration.py --- a/common/migration.py Mon Sep 14 11:25:56 2009 +0200 +++ b/common/migration.py Tue Sep 15 15:01:41 2009 +0200 @@ -228,7 +228,10 @@ else: readline.set_completer(Completer(local_ctx).complete) readline.parse_and_bind('tab: complete') - histfile = os.path.join(os.environ["HOME"], ".eshellhist") + home_key = 'HOME' + if sys.platform == 'win32': + home_key = 'USERPROFILE' + histfile = os.path.join(os.environ[home_key], ".eshellhist") try: readline.read_history_file(histfile) except IOError: diff -r fc63b80ec979 -r 1d25e928c299 common/uilib.py --- a/common/uilib.py Mon Sep 14 11:25:56 2009 +0200 +++ b/common/uilib.py Tue Sep 15 15:01:41 2009 +0200 @@ -31,7 +31,7 @@ return 'Any X WHERE X eid %s' % eid -def printable_value(req, attrtype, value, props=None): +def printable_value(req, attrtype, value, props=None, displaytime=True): """return a displayable value (i.e. unicode string)""" if value is None or attrtype == 'Bytes': return u'' @@ -46,7 +46,9 @@ if attrtype == 'Time': return ustrftime(value, req.property_value('ui.time-format')) if attrtype == 'Datetime': - return ustrftime(value, req.property_value('ui.datetime-format')) + if displaytime: + return ustrftime(value, req.property_value('ui.datetime-format')) + return ustrftime(value, req.property_value('ui.date-format')) if attrtype == 'Boolean': if value: return req._('yes') diff -r fc63b80ec979 -r 1d25e928c299 cwvreg.py --- a/cwvreg.py Mon Sep 14 11:25:56 2009 +0200 +++ b/cwvreg.py Tue Sep 15 15:01:41 2009 +0200 @@ -138,7 +138,6 @@ baseschemas = [eschema] + eschema.ancestors() # browse ancestors from most specific to most generic and try to find an # associated custom entity class - cls = None for baseschema in baseschemas: try: btype = ETYPE_NAME_MAP[baseschema] diff -r fc63b80ec979 -r 1d25e928c299 devtools/devctl.py --- a/devtools/devctl.py Mon Sep 14 11:25:56 2009 +0200 +++ b/devtools/devctl.py Tue Sep 15 15:01:41 2009 +0200 @@ -153,7 +153,8 @@ add_msg(w, rschema.description) w('# add related box generated message\n') w('\n') - actionbox = vreg['boxes']['edit_box'][0] + from cubicweb.web import uicfg + appearsin_addmenu = uicfg.actionbox_appearsin_addmenu for eschema in schema.entities(): if eschema.is_final(): continue @@ -172,8 +173,8 @@ subjtype, objtype = teschema, eschema if librschema.has_rdef(subjtype, objtype): continue - if actionbox.appearsin_addmenu.etype_get(eschema, rschema, - role, teschema): + if appearsin_addmenu.etype_get(eschema, rschema, role, + teschema): if role == 'subject': label = 'add %s %s %s %s' % (eschema, rschema, teschema, role) diff -r fc63b80ec979 -r 1d25e928c299 devtools/testlib.py --- a/devtools/testlib.py Mon Sep 14 11:25:56 2009 +0200 +++ b/devtools/testlib.py Tue Sep 15 15:01:41 2009 +0200 @@ -408,6 +408,23 @@ res.setdefault(a.category, []).append(a.__class__) return res + def action_submenu(self, req, rset, id): + return self._test_action(self.vreg['actions'].select(id, req, rset=rset)) + + def _test_action(self, action): + class fake_menu(list): + @property + def items(self): + return self + class fake_box(object): + def mk_action(self, label, url, **kwargs): + return (label, url) + def box_action(self, action, **kwargs): + return (action.title, action.url()) + submenu = fake_menu() + action.fill_menu(fake_box(), submenu) + return submenu + def list_views_for(self, rset): """returns the list of views that can be applied on `rset`""" req = rset.req @@ -784,7 +801,7 @@ # resultset's syntax tree rset = backup_rset for action in self.list_actions_for(rset): - yield InnerTest(self._testname(rset, action.id, 'action'), action.url) + yield InnerTest(self._testname(rset, action.id, 'action'), self._test_action, action) for box in self.list_boxes_for(rset): yield InnerTest(self._testname(rset, box.id, 'box'), box.render) diff -r fc63b80ec979 -r 1d25e928c299 entities/lib.py --- a/entities/lib.py Mon Sep 14 11:25:56 2009 +0200 +++ b/entities/lib.py Tue Sep 15 15:01:41 2009 +0200 @@ -10,7 +10,7 @@ from urlparse import urlsplit, urlunsplit from datetime import datetime -from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated from cubicweb import UnknownProperty from cubicweb.entity import _marker @@ -25,7 +25,7 @@ class EmailAddress(AnyEntity): id = 'EmailAddress' - fetch_attrs, fetch_order = fetch_config(['address', 'alias', 'canonical']) + fetch_attrs, fetch_order = fetch_config(['address', 'alias']) def dc_title(self): if self.alias: @@ -36,15 +36,13 @@ def email_of(self): return self.reverse_use_email and self.reverse_use_email[0] - @cached + @property + def prefered(self): + return self.prefered_form and self.prefered_form[0] or self + + @deprecated('use .prefered') def canonical_form(self): - if self.canonical: - return self - rql = 'EmailAddress X WHERE X identical_to Y, X canonical TRUE, Y eid %(y)s' - cnrset = self.req.execute(rql, {'y': self.eid}, 'y') - if cnrset: - return cnrset.get_entity(0, 0) - return None + return self.prefered_form and self.prefered_form[0] or self def related_emails(self, skipeids=None): # XXX move to eemail diff -r fc63b80ec979 -r 1d25e928c299 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Mon Sep 14 11:25:56 2009 +0200 +++ b/entities/test/unittest_base.py Tue Sep 15 15:01:41 2009 +0200 @@ -58,16 +58,13 @@ class EmailAddressTC(BaseEntityTC): def test_canonical_form(self): - eid1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0] - eid2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com", X canonical TRUE')[0][0] - self.execute('SET X identical_to Y WHERE X eid %s, Y eid %s' % (eid1, eid2)) - email1 = self.entity('Any X WHERE X eid %(x)s', {'x':eid1}, 'x') - email2 = self.entity('Any X WHERE X eid %(x)s', {'x':eid2}, 'x') - self.assertEquals(email1.canonical_form().eid, eid2) - self.assertEquals(email2.canonical_form(), email2) - eid3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"')[0][0] - email3 = self.entity('Any X WHERE X eid %s'%eid3) - self.assertEquals(email3.canonical_form(), None) + email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0) + email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0) + email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0) + self.execute('SET X prefered_form Y WHERE X eid %s, Y eid %s' % (email1.eid, email2.eid)) + self.assertEquals(email1.prefered.eid, email2.eid) + self.assertEquals(email2.prefered.eid, email2.eid) + self.assertEquals(email3.prefered.eid, email3.eid) def test_mangling(self): eid = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"')[0][0] @@ -108,10 +105,13 @@ self.vreg.register_appobject_class(MyUser) self.vreg['etypes'].initialization_completed() MyUser_ = self.vreg['etypes'].etype_class('CWUser') - self.failIf(MyUser is MyUser_.__bases__) - self.failUnless(MyUser in MyUser_.__bases__) + # a copy is done systematically + self.failUnless(issubclass(MyUser_, MyUser)) self.failUnless(implements(MyUser_, IMileStone)) self.failUnless(implements(MyUser_, IWorkflowable)) + # original class should not have beed modified, only the copy + self.failUnless(implements(MyUser, IMileStone)) + self.failIf(implements(MyUser, IWorkflowable)) class SpecializedEntityClassesTC(CubicWebTC): diff -r fc63b80ec979 -r 1d25e928c299 entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Mon Sep 14 11:25:56 2009 +0200 +++ b/entities/test/unittest_wfobjs.py Tue Sep 15 15:01:41 2009 +0200 @@ -331,12 +331,12 @@ self.assertEquals(self.member.state, 'asleep')# no change before commit self.commit() self.member.clear_all_caches() - self.assertEquals(self.member.current_workflow.name, "CWUser workflow") + self.assertEquals(self.member.current_workflow.name, "default user workflow") self.assertEquals(self.member.state, 'activated') self.assertEquals(parse_hist(self.member.workflow_history), [('activated', 'deactivated', 'deactivate', None), ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'), - ('asleep', 'activated', None, 'workflow changed to "CWUser workflow"'),]) + ('asleep', 'activated', None, 'workflow changed to "default user workflow"'),]) class WorkflowHooksTC(CubicWebTC): diff -r fc63b80ec979 -r 1d25e928c299 entity.py --- a/entity.py Mon Sep 14 11:25:56 2009 +0200 +++ b/entity.py Tue Sep 15 15:01:41 2009 +0200 @@ -321,7 +321,7 @@ return value def printable_value(self, attr, value=_marker, attrtype=None, - format='text/html'): + format='text/html', displaytime=True): """return a displayable value (i.e. unicode string) which may contains html tags """ @@ -351,7 +351,8 @@ return self.mtc_transform(value.getvalue(), attrformat, format, encoding) return u'' - value = printable_value(self.req, attrtype, value, props) + value = printable_value(self.req, attrtype, value, props, + displaytime=displaytime) if format == 'text/html': value = xml_escape(value) return value diff -r fc63b80ec979 -r 1d25e928c299 misc/migration/3.5.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.5.0_Any.py Tue Sep 15 15:01:41 2009 +0200 @@ -0,0 +1,7 @@ +add_relation_type('prefered_form') + +rql('SET X prefered_form Y WHERE Y canonical TRUE, X identical_to Y') +checkpoint() + +drop_attribute('EmailAddress', 'canonical') +drop_relation_definition('EmailAddress', 'identical_to', 'EmailAddress') diff -r fc63b80ec979 -r 1d25e928c299 misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Mon Sep 14 11:25:56 2009 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Tue Sep 15 15:01:41 2009 +0200 @@ -32,7 +32,17 @@ add_entity_type('BaseTransition') add_entity_type('WorkflowTransition') add_entity_type('SubWorkflowExitPoint') - drop_relation_definition('State', 'allowed_transition', 'Transition') # should be infered + # drop explicit 'State allowed_transition Transition' since it should be + # infered due to yams inheritance. However we've to disable the schema + # sync hook first to avoid to destroy existing data... + from cubicweb.server.schemahooks import after_del_relation_type + repo.hm.unregister_hook(after_del_relation_type, + 'after_delete_relation', 'relation_type') + try: + drop_relation_definition('State', 'allowed_transition', 'Transition') + finally: + repo.hm.register_hook(after_del_relation_type, + 'after_delete_relation', 'relation_type') schema.rebuild_infered_relations() # need to be explicitly called once everything is in place for et in rql('DISTINCT Any ET,ETN WHERE S state_of ET, ET name ETN', diff -r fc63b80ec979 -r 1d25e928c299 schemas/base.py --- a/schemas/base.py Mon Sep 14 11:25:56 2009 +0200 +++ b/schemas/base.py Tue Sep 15 15:01:41 2009 +0200 @@ -52,11 +52,10 @@ alias = String(fulltextindexed=True, maxsize=56) address = String(required=True, fulltextindexed=True, indexed=True, unique=True, maxsize=128) - canonical = Boolean(default=False, - description=_('when multiple addresses are equivalent \ + prefered_form = SubjectRelation('EmailAddress', cardinality='?*', + description=_('when multiple addresses are equivalent \ (such as python-projects@logilab.org and python-projects@lists.logilab.org), set this \ -to true on one of them which is the preferred form.')) - identical_to = SubjectRelation('EmailAddress') +to indicate which is the preferred form.')) class use_email(RelationType): """ """ @@ -71,9 +70,7 @@ """the prefered email""" permissions = use_email.permissions -class identical_to(RelationType): - """identical_to""" - symetric = True +class prefered_form(RelationType): permissions = { 'read': ('managers', 'users', 'guests',), # XXX should have update permissions on both subject and object, @@ -207,10 +204,6 @@ } -class see_also(RelationType): - """generic relation to link one entity to another""" - symetric = True - class ExternalUri(EntityType): """a URI representing an object in external data store""" uri = String(required=True, unique=True, maxsize=256, @@ -254,3 +247,25 @@ name = String(required=True, unique=True, indexed=True, maxsize=128, description=_('name of the cache')) timestamp = Datetime(default='NOW') + + +# "abtract" relation types, not used in cubicweb itself + +class identical_to(RelationType): + """identical to""" + symetric = True + permissions = { + 'read': ('managers', 'users', 'guests',), + # XXX should have update permissions on both subject and object, + # though by doing this we will probably have no way to add + # this relation in the web ui. The easiest way to acheive this + # is probably to be able to have "U has_update_permission O" as + # RQLConstraint of the relation definition, though this is not yet + # possible + 'add': ('managers', RRQLExpression('U has_update_permission S'),), + 'delete': ('managers', RRQLExpression('U has_update_permission S'),), + } + +class see_also(RelationType): + """generic relation to link one entity to another""" + symetric = True diff -r fc63b80ec979 -r 1d25e928c299 server/migractions.py --- a/server/migractions.py Mon Sep 14 11:25:56 2009 +0200 +++ b/server/migractions.py Tue Sep 15 15:01:41 2009 +0200 @@ -415,11 +415,12 @@ espschema = eschema.specializes() if repospschema and not espschema: self.rqlexec('DELETE X specializes Y WHERE X is CWEType, X name %(x)s', - {'x': str(repoeschema)}) + {'x': str(repoeschema)}, ask_confirm=False) elif not repospschema and espschema: self.rqlexec('SET X specializes Y WHERE X is CWEType, X name %(x)s, ' 'Y is CWEType, Y name %(y)s', - {'x': str(repoeschema), 'y': str(espschema)}) + {'x': str(repoeschema), 'y': str(espschema)}, + ask_confirm=False) self.rqlexecall(ss.updateeschema2rql(eschema), ask_confirm=self.verbosity >= 2) for rschema, targettypes, role in eschema.relation_definitions(True): diff -r fc63b80ec979 -r 1d25e928c299 test/unittest_schema.py --- a/test/unittest_schema.py Mon Sep 14 11:25:56 2009 +0200 +++ b/test/unittest_schema.py Tue Sep 15 15:01:41 2009 +0200 @@ -162,7 +162,7 @@ expected_relations = ['add_permission', 'address', 'alias', 'allowed_transition', 'bookmarked_by', 'by_transition', - 'canonical', 'cardinality', 'comment', 'comment_format', + 'cardinality', 'comment', 'comment_format', 'composite', 'condition', 'connait', 'constrained_by', 'content', 'content_format', 'created_by', 'creation_date', 'cstrtype', 'custom_workflow', 'cwuri', @@ -175,7 +175,7 @@ 'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed', 'has_text', - 'identical_to', 'identity', 'in_group', 'in_state', 'indexed', + 'identity', 'in_group', 'in_state', 'indexed', 'initial_state', 'inlined', 'internationalizable', 'is', 'is_instance_of', 'label', 'last_login_time', 'login', @@ -186,7 +186,7 @@ 'ordernum', 'owned_by', - 'path', 'pkey', 'prenom', 'primary_email', + 'path', 'pkey', 'prefered_form', 'prenom', 'primary_email', 'read_permission', 'relation_type', 'require_group', diff -r fc63b80ec979 -r 1d25e928c299 web/action.py --- a/web/action.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/action.py Tue Sep 15 15:01:41 2009 +0200 @@ -31,8 +31,18 @@ 'useractions', 'siteactions', 'hidden'), help=_('context where this component should be displayed')), } - site_wide = True # don't want user to configuration actions eproperties + site_wide = True # don't want user to configurate actions category = 'moreactions' + # actions in category 'moreactions' can specify a sub-menu in which they should be filed + submenu = None + + def actual_actions(self): + yield self + + def fill_menu(self, box, menu): + """add action(s) to the given submenu of the given box""" + for action in self.actual_actions(): + menu.append(box.box_action(action)) def url(self): """return the url associated with this action""" @@ -44,6 +54,9 @@ if self.category: return 'box' + self.category.capitalize() + def build_action(self, title, path, **kwargs): + return UnregisteredAction(self.req, self.rset, title, path, **kwargs) + class UnregisteredAction(Action): """non registered action used to build boxes. Unless you set them @@ -72,7 +85,8 @@ __select__ = (match_search_state('normal') & one_line_rset() & partial_relation_possible(action='add') & partial_may_add_relation()) - category = 'addrelated' + + submenu = 'addrelated' def url(self): current_entity = self.rset.get_entity(self.row or 0, self.col or 0) diff -r fc63b80ec979 -r 1d25e928c299 web/box.py --- a/web/box.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/box.py Tue Sep 15 15:01:41 2009 +0200 @@ -57,7 +57,8 @@ result = [] actions_by_cat = {} for action in actions: - actions_by_cat.setdefault(action.category, []).append((action.title, action)) + actions_by_cat.setdefault(action.category, []).append( + (action.title, action) ) for key, values in actions_by_cat.items(): actions_by_cat[key] = [act for title, act in sorted(values)] for cat in self.categories_in_order: diff -r fc63b80ec979 -r 1d25e928c299 web/test/unittest_breadcrumbs.py --- a/web/test/unittest_breadcrumbs.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/test/unittest_breadcrumbs.py Tue Sep 15 15:01:41 2009 +0200 @@ -8,10 +8,10 @@ self.execute('SET F2 filed_under F1 WHERE F1 eid %(f1)s, F2 eid %(f2)s', {'f1' : f1.eid, 'f2' : f2.eid}) self.commit() - childrset = self.execute('Folder F WHERE F eid %s' % f2.eid) - self.assertEquals(childrset.get_entity(0,0).view('breadcrumbs'), - 'chi&ld' % f1.eid) + self.assertEquals(f2.view('breadcrumbs'), + 'chi&ld' % f2.eid) + childrset = f2.as_rset() ibc = self.vreg['components'].select('breadcrumbs', self.request(), rset=childrset) self.assertEquals(ibc.render(), """ > folder_plural > par&ent >  -chi&ld""" % f2.eid) +chi&ld""" % f1.eid) diff -r fc63b80ec979 -r 1d25e928c299 web/test/unittest_views_navigation.py --- a/web/test/unittest_views_navigation.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/test/unittest_views_navigation.py Tue Sep 15 15:01:41 2009 +0200 @@ -95,37 +95,36 @@ html = navcomp.render() - -class ContentNavigationTC(CubicWebTC): +# XXX deactivate, contextual component has been removed +# class ContentNavigationTC(CubicWebTC): + # def test_component_context(self): + # view = mock_object(is_primary=lambda x: True) + # rset = self.execute('CWUser X LIMIT 1') + # req = self.request() + # objs = self.vreg['contentnavigation'].poss_visible_objects( + # req, rset=rset, view=view, context='navtop') + # # breadcrumbs should be in headers by default + # clsids = set(obj.id for obj in objs) + # self.failUnless('breadcrumbs' in clsids) + # objs = self.vreg['contentnavigation'].poss_visible_objects( + # req, rset=rset, view=view, context='navbottom') + # # breadcrumbs should _NOT_ be in footers by default + # clsids = set(obj.id for obj in objs) + # self.failIf('breadcrumbs' in clsids) + # self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", ' + # 'P value "navbottom"') + # # breadcrumbs should now be in footers + # req.cnx.commit() + # objs = self.vreg['contentnavigation'].poss_visible_objects( + # req, rset=rset, view=view, context='navbottom') - def test_component_context(self): - view = mock_object(is_primary=lambda x: True) - rset = self.execute('CWUser X LIMIT 1') - req = self.request() - objs = self.vreg['contentnavigation'].poss_visible_objects( - req, rset=rset, view=view, context='navtop') - # breadcrumbs should be in headers by default - clsids = set(obj.id for obj in objs) - self.failUnless('breadcrumbs' in clsids) - objs = self.vreg['contentnavigation'].poss_visible_objects( - req, rset=rset, view=view, context='navbottom') - # breadcrumbs should _NOT_ be in footers by default - clsids = set(obj.id for obj in objs) - self.failIf('breadcrumbs' in clsids) - self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", ' - 'P value "navbottom"') - # breadcrumbs should now be in footers - req.cnx.commit() - objs = self.vreg['contentnavigation'].poss_visible_objects( - req, rset=rset, view=view, context='navbottom') + # clsids = [obj.id for obj in objs] + # self.failUnless('breadcrumbs' in clsids) + # objs = self.vreg['contentnavigation'].poss_visible_objects( + # req, rset=rset, view=view, context='navtop') - clsids = [obj.id for obj in objs] - self.failUnless('breadcrumbs' in clsids) - objs = self.vreg['contentnavigation'].poss_visible_objects( - req, rset=rset, view=view, context='navtop') - - clsids = [obj.id for obj in objs] - self.failIf('breadcrumbs' in clsids) + # clsids = [obj.id for obj in objs] + # self.failIf('breadcrumbs' in clsids) if __name__ == '__main__': diff -r fc63b80ec979 -r 1d25e928c299 web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/test/unittest_viewselector.py Tue Sep 15 15:01:41 2009 +0200 @@ -17,6 +17,8 @@ treeview, idownloadable, wdoc, debug, cwproperties, workflow, xmlrss, csvexport) +from cubes.folder import views as folderviews + USERACTIONS = [('myprefs', actions.UserPreferencesAction), ('myinfos', actions.UserInfoAction), ('logout', actions.LogoutAction)] @@ -76,7 +78,9 @@ ('propertiesform', cwproperties.CWPropertiesForm), ('registry', startup.RegistryView), ('schema', schema.SchemaView), - ('systempropertiesform', cwproperties.SystemCWPropertiesForm)]) + ('systempropertiesform', cwproperties.SystemCWPropertiesForm), + ('tree', folderviews.FolderTreeView), + ]) def test_possible_views_noresult(self): rset, req = self.rset_and_req('Any X WHERE X eid 999999') @@ -254,9 +258,9 @@ self.assertDictEqual(self.pactions(req, rset), {'useractions': USERACTIONS, 'siteactions': SITEACTIONS, - 'mainactions': [('edit', actions.ModifyAction), - ('workflow', workflow.ViewWorkflowAction),], + 'mainactions': [('edit', actions.ModifyAction)], 'moreactions': [('managepermission', actions.ManagePermissionsAction), + ('addrelated', actions.AddRelatedActions), ('delete', actions.DeleteAction), ('copy', actions.CopyAction), ], @@ -445,6 +449,7 @@ 'siteactions': SITEACTIONS, 'mainactions': [('edit', actions.ModifyAction)], 'moreactions': [('managepermission', actions.ManagePermissionsAction), + ('addrelated', actions.AddRelatedActions), ('delete', actions.DeleteAction), ('copy', actions.CopyAction), ('testaction', CWETypeRQLAction), @@ -456,6 +461,7 @@ 'siteactions': SITEACTIONS, 'mainactions': [('edit', actions.ModifyAction)], 'moreactions': [('managepermission', actions.ManagePermissionsAction), + ('addrelated', actions.AddRelatedActions), ('delete', actions.DeleteAction), ('copy', actions.CopyAction), ], diff -r fc63b80ec979 -r 1d25e928c299 web/views/actions.py --- a/web/views/actions.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/actions.py Tue Sep 15 15:01:41 2009 +0200 @@ -225,6 +225,71 @@ return self.build_url('add/%s' % self.rsettype) +class AddRelatedActions(Action): + """fill 'addrelated' sub-menu of the actions box""" + id = 'addrelated' + __select__ = Action.__select__ & one_line_rset() & non_final_entity() + + submenu = _('addrelated') + order = 20 + + 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.req._('add') + super(AddRelatedActions, self).fill_menu(box, menu) + + def actual_actions(self): + entity = self.rset.get_entity(self.row or 0, self.col or 0) + eschema = entity.e_schema + for rschema, teschema, x in self.add_related_schemas(entity): + if x == 'subject': + label = 'add %s %s %s %s' % (eschema, rschema, teschema, x) + url = self.linkto_url(entity, rschema, teschema, 'object') + else: + label = 'add %s %s %s %s' % (teschema, rschema, eschema, x) + url = self.linkto_url(entity, rschema, teschema, 'subject') + yield self.build_action(self.req._(label), url) + + def add_related_schemas(self, entity): + """this is actually used ui method to generate 'addrelated' actions from + the schema. + + If you don't want any auto-generated actions, you should overrides this + method to return an empty list. If you only want some, you can configure + them by using uicfg.actionbox_appearsin_addmenu + """ + appearsin_addmenu = uicfg.actionbox_appearsin_addmenu + req = self.req + eschema = entity.e_schema + for role, rschemas in (('subject', eschema.subject_relations()), + ('object', eschema.object_relations())): + for rschema in rschemas: + if rschema.is_final(): + continue + # check the relation can be added as well + # XXX consider autoform_permissions_overrides? + if role == 'subject'and not rschema.has_perm(req, 'add', + fromeid=entity.eid): + continue + if role == 'object'and not rschema.has_perm(req, 'add', + toeid=entity.eid): + continue + # check the target types can be added as well + for teschema in rschema.targets(eschema, role): + if not appearsin_addmenu.etype_get(eschema, rschema, + role, teschema): + continue + if teschema.has_local_role('add') or teschema.has_perm(req, 'add'): + yield rschema, teschema, role + + def linkto_url(self, entity, rtype, etype, target): + return self.build_url(vid='creation', etype=etype, + __linkto='%s:%s:%s' % (rtype, entity.eid, target), + __redirectpath=entity.rest_path(), # should not be url quoted! + __redirectvid=self.req.form.get('vid', '')) + + # logged user actions ######################################################### class UserPreferencesAction(Action): diff -r fc63b80ec979 -r 1d25e928c299 web/views/boxes.py --- a/web/views/boxes.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/boxes.py Tue Sep 15 15:01:41 2009 +0200 @@ -16,6 +16,8 @@ __docformat__ = "restructuredtext en" _ = unicode +from warnings import warn + from logilab.mtconverter import xml_escape from cubicweb.selectors import match_user_groups, non_final_entity @@ -26,7 +28,7 @@ from cubicweb.web.box import BoxTemplate -class EditBox(BoxTemplate): +class EditBox(BoxTemplate): # XXX rename to ActionsBox """ box with all actions impacting the entity displayed: edit, copy, delete change state, add related entities @@ -36,9 +38,6 @@ title = _('actions') order = 2 - # class attributes below are actually stored in the uicfg module since we - # don't want them to be reloaded - appearsin_addmenu = uicfg.actionbox_appearsin_addmenu def call(self, view=None, **kwargs): _ = self.req._ @@ -50,111 +49,59 @@ etypelabel = display_name(self.req, iter(etypes).next(), plural) title = u'%s - %s' % (title, etypelabel.lower()) box = BoxWidget(title, self.id, _class="greyBoxFrame") + self._menus_in_order = [] + self._menus_by_id = {} # build list of actions actions = self.vreg['actions'].possible_actions(self.req, self.rset, view=view) - add_menu = BoxMenu(_('add')) # 'addrelated' category - other_menu = BoxMenu(_('more actions')) # 'moreactions' category - searchstate = self.req.search_state[0] - for category, menu in (('mainactions', box), - ('addrelated', add_menu), - ('moreactions', other_menu)): + other_menu = self._get_menu('moreactions', _('more actions')) + for category, defaultmenu in (('mainactions', box), + ('moreactions', other_menu), + ('addrelated', None)): for action in actions.get(category, ()): - menu.append(self.box_action(action)) - if self.rset and self.rset.rowcount == 1 and \ - not self.schema[self.rset.description[0][0]].is_final() and \ - searchstate == 'normal': - entity = self.rset.get_entity(0, 0) - for action in self.schema_actions(entity): - add_menu.append(action) - self.workflow_actions(entity, box) + if category == 'addrelated': + warn('"addrelated" category is deprecated, use "moreaction"' + ' category w/ "addrelated" submenu', + DeprecationWarning) + defaultmenu = self._get_menu('addrelated', _('add'), _('add')) + if action.submenu: + menu = self._get_menu(action.submenu) + else: + menu = defaultmenu + action.fill_menu(self, menu) if box.is_empty() and not other_menu.is_empty(): box.items = other_menu.items other_menu.items = [] - self.add_submenu(box, add_menu, _('add')) - self.add_submenu(box, other_menu) + else: # ensure 'more actions' menu appears last + self._menus_in_order.remove(other_menu) + self._menus_in_order.append(other_menu) + for submenu in self._menus_in_order: + self.add_submenu(box, submenu) if not box.is_empty(): box.render(self.w) + def _get_menu(self, id, title=None, label_prefix=None): + try: + return self._menus_by_id[id] + except KeyError: + if title is None: + title = self.req._(id) + self._menus_by_id[id] = menu = BoxMenu(title) + menu.label_prefix = label_prefix + self._menus_in_order.append(menu) + return menu + def add_submenu(self, box, submenu, label_prefix=None): - if len(submenu.items) == 1: + appendanyway = getattr(submenu, 'append_anyway', False) + if len(submenu.items) == 1 and not appendanyway: boxlink = submenu.items[0] - if label_prefix: - boxlink.label = u'%s %s' % (label_prefix, boxlink.label) + if submenu.label_prefix: + boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label) box.append(boxlink) elif submenu.items: box.append(submenu) - - def schema_actions(self, entity): - user = self.req.user - actions = [] - _ = self.req._ - eschema = entity.e_schema - for rschema, teschema, x in self.add_related_schemas(entity): - if x == 'subject': - label = 'add %s %s %s %s' % (eschema, rschema, teschema, x) - url = self.linkto_url(entity, rschema, teschema, 'object') - else: - label = 'add %s %s %s %s' % (teschema, rschema, eschema, x) - url = self.linkto_url(entity, rschema, teschema, 'subject') - actions.append(self.mk_action(_(label), url)) - return actions - - def add_related_schemas(self, entity): - """this is actually used ui method to generate 'addrelated' actions from - the schema. - - If you don't want any auto-generated actions, you should overrides this - method to return an empty list. If you only want some, you can configure - them by using uicfg.actionbox_appearsin_addmenu - """ - req = self.req - eschema = entity.e_schema - for role, rschemas in (('subject', eschema.subject_relations()), - ('object', eschema.object_relations())): - for rschema in rschemas: - if rschema.is_final(): - continue - # check the relation can be added as well - # XXX consider autoform_permissions_overrides? - if role == 'subject'and not rschema.has_perm(req, 'add', - fromeid=entity.eid): - continue - if role == 'object'and not rschema.has_perm(req, 'add', - toeid=entity.eid): - continue - # check the target types can be added as well - for teschema in rschema.targets(eschema, role): - if not self.appearsin_addmenu.etype_get(eschema, rschema, - role, teschema): - continue - if teschema.has_local_role('add') or teschema.has_perm(req, 'add'): - yield rschema, teschema, role - - - def workflow_actions(self, entity, box): - if entity.e_schema.has_subject_relation('in_state') and entity.in_state: - _ = self.req._ - menu_title = u'%s: %s' % (_('state'), entity.printable_state) - menu_items = [] - for tr in entity.possible_transitions(): - url = entity.absolute_url(vid='statuschange', treid=tr.eid) - menu_items.append(self.mk_action(_(tr.name), url)) - # don't propose to see wf if user can't pass any transition - if menu_items: - wfurl = self.build_url('cwetype/%s'%entity.e_schema, vid='workflow') - menu_items.append(self.mk_action(_('view workflow'), wfurl)) - if entity.workflow_history: - wfurl = entity.absolute_url(vid='wfhistory') - menu_items.append(self.mk_action(_('view history'), wfurl)) - box.append(BoxMenu(menu_title, menu_items)) - return None - - def linkto_url(self, entity, rtype, etype, target): - return self.build_url(vid='creation', etype=etype, - __linkto='%s:%s:%s' % (rtype, entity.eid, target), - __redirectpath=entity.rest_path(), # should not be url quoted! - __redirectvid=self.req.form.get('vid', '')) + elif appendanyway: + box.append(RawBoxItem(xml_escape(submenu.label))) class SearchBox(BoxTemplate): diff -r fc63b80ec979 -r 1d25e928c299 web/views/emailaddress.py --- a/web/views/emailaddress.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/emailaddress.py Tue Sep 15 15:01:41 2009 +0200 @@ -24,17 +24,9 @@ def render_entity_attributes(self, entity): self.w(u'

') entity.view('oneline', w=self.w) - if not entity.canonical: - canonemailaddr = entity.canonical_form() - if canonemailaddr: - self.w(u' (%s)' % canonemailaddr.view('oneline')) - self.w(u'

') - elif entity.identical_to: - self.w(u'') - identicaladdr = [e.view('oneline') for e in entity.identical_to] - self.field('identical_to', ', '.join(identicaladdr)) - else: - self.w(u'') + if entity.prefered: + self.w(u' (%s)' % entity.prefered.view('oneline')) + self.w(u'') try: persons = entity.reverse_primary_email except Unauthorized: @@ -88,6 +80,7 @@ if entity.reverse_primary_email: self.w(u'') + class EmailAddressMailToView(baseviews.OneLineView): """A one line view that builds a user clickable URL for an email with 'mailto:'""" diff -r fc63b80ec979 -r 1d25e928c299 web/views/management.py --- a/web/views/management.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/management.py Tue Sep 15 15:01:41 2009 +0200 @@ -237,10 +237,8 @@ w(u"Package %s version: %s
\n" % (cube, cubeversion)) cversions.append((cube, cubeversion)) w(u"") - # creates a bug submission link if SUBMIT_URL is set - submiturl = self.config['submit-url'] - submitmail = self.config['submit-mail'] - if submiturl or submitmail: + # creates a bug submission link if submit-mail is set + if self.config['submit-mail']: form = self.vreg['forms'].select('base', self.req, rset=None, mainform=False) binfo = text_error_description(ex, excinfo, req, eversion, cversions) @@ -248,15 +246,9 @@ # we must use a text area to keep line breaks widget=wdgs.TextArea({'class': 'hidden'})) form.form_add_hidden('__bugreporting', '1') - if submitmail: - form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)] - form.action = req.build_url('reportbug') - w(form.form_render()) - if submiturl: - form.form_add_hidden('description_format', 'text/rest') - form.form_buttons = [wdgs.SubmitButton(SUBMIT_MSGID)] - form.action = submiturl - w(form.form_render()) + form.form_buttons = [wdgs.SubmitButton(MAIL_SUBMIT_MSGID)] + form.action = req.build_url('reportbug') + w(form.form_render()) def exc_message(ex, encoding): diff -r fc63b80ec979 -r 1d25e928c299 web/views/schema.py --- a/web/views/schema.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/schema.py Tue Sep 15 15:01:41 2009 +0200 @@ -12,7 +12,8 @@ from logilab.mtconverter import xml_escape from yams import BASE_TYPES, schema2dot as s2d -from cubicweb.selectors import implements, yes, match_user_groups +from cubicweb.selectors import (implements, yes, match_user_groups, + has_related_entities) from cubicweb.schema import META_RTYPES, SCHEMA_TYPES from cubicweb.schemaviewer import SchemaViewer from cubicweb.view import EntityView, StartupView @@ -269,6 +270,7 @@ xml_escape(url), xml_escape(self.req._('graphical schema for %s') % entity.name))) + class CWETypeSPermView(EntityView): id = 'cwetype-schema-permissions' __select__ = EntityView.__select__ & implements('CWEType') @@ -296,18 +298,29 @@ {'x': entity.eid}) self.wview('outofcontext', rset, 'null') + class CWETypeSWorkflowView(EntityView): id = 'cwetype-workflow' - __select__ = EntityView.__select__ & implements('CWEType') + __select__ = (EntityView.__select__ & implements('CWEType') & + has_related_entities('workflow_of', 'object')) def cell_call(self, row, col): entity = self.rset.get_entity(row, col) - if entity.reverse_state_of: - self.w(u'%s' % ( - xml_escape(entity.absolute_url(vid='ewfgraph')), - xml_escape(self.req._('graphical workflow for %s') % entity.name))) - else: - self.w(u'

%s

' % _('There is no workflow defined for this entity.')) + if entity.default_workflow: + wf = entity.default_workflow[0] + self.w(u'

%s (%s)

' % (wf.name, self.req._('default'))) + self.wf_image(wf) + for altwf in entity.reverse_workflow_of: + if altwf.eid == wf.eid: + continue + self.w(u'

%s

' % altwf.name) + self.wf_image(altwf) + + def wf_image(self, wf): + self.w(u'%s' % ( + xml_escape(wf.absolute_url(vid='wfgraph')), + xml_escape(self.req._('graphical representation of %s') % wf.name))) + # CWRType ###################################################################### diff -r fc63b80ec979 -r 1d25e928c299 web/views/tableview.py --- a/web/views/tableview.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/tableview.py Tue Sep 15 15:01:41 2009 +0200 @@ -144,8 +144,11 @@ actions += self.show_hide_actions(divid, True) self.w(u'
') # close
') # close
') for tab in tabs: w(u'
  • ') - w(u'' % tab) + w(u'' % tab) w(u'' % (tab, self.cookie_name)) w(self.req._(tab)) w(u'') @@ -121,7 +124,7 @@ w(u'') w(u'
  • ') for tab in tabs: - w(u'
    ' % tab) + w(u'
    ' % tab) if entity: self.lazyview(tab, eid=entity.eid) else: @@ -153,7 +156,7 @@ role = 'subject' vid = 'gallery' - in this example, entities related to project entity by the'screenshot' + in this example, entities related to project entity by the 'screenshot' relation (where the project is subject of the relation) will be displayed using the 'gallery' view. """ diff -r fc63b80ec979 -r 1d25e928c299 web/views/workflow.py --- a/web/views/workflow.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/views/workflow.py Tue Sep 15 15:01:41 2009 +0200 @@ -15,13 +15,14 @@ from logilab.common.graph import escape, GraphGenerator, DotBackend from cubicweb import Unauthorized, view -from cubicweb.selectors import (implements, has_related_entities, +from cubicweb.selectors import (implements, has_related_entities, one_line_rset, relation_possible, match_form_params) from cubicweb.interfaces import IWorkflowable from cubicweb.view import EntityView -from cubicweb.web import stdmsgs, action, component, form +from cubicweb.schema import display_name +from cubicweb.web import stdmsgs, action, component, form, action from cubicweb.web import formfields as ff, formwidgets as fwdgs -from cubicweb.web.views import TmpFileViewMixin, forms +from cubicweb.web.views import TmpFileViewMixin, forms, primary # IWorkflowable views ######################################################### @@ -109,7 +110,41 @@ def cell_call(self, row, col, view=None): self.wview('wfhistory', self.rset, row=row, col=col, view=view) -# workflow entity types views ################################################# + +# workflow actions ############################################################# + +class WorkflowActions(action.Action): + """fill 'workflow' sub-menu of the actions box""" + id = 'workflow' + __select__ = (action.Action.__select__ & one_line_rset() & + relation_possible('in_state')) + + submenu = _('workflow') + order = 10 + + def fill_menu(self, box, menu): + entity = self.rset.get_entity(self.row or 0, self.col or 0) + menu.label = u'%s: %s' % (self.req._('state'), entity.printable_state) + menu.append_anyway = True + super(WorkflowActions, self).fill_menu(box, menu) + + def actual_actions(self): + entity = self.rset.get_entity(self.row or 0, self.col or 0) + hastr = False + for tr in entity.possible_transitions(): + url = entity.absolute_url(vid='statuschange', treid=tr.eid) + yield self.build_action(self.req._(tr.name), url) + hastr = True + # don't propose to see wf if user can't pass any transition + if hastr: + wfurl = entity.current_workflow.absolute_url() + yield self.build_action(self.req._('view workflow'), wfurl) + if entity.workflow_history: + wfurl = entity.absolute_url(vid='wfhistory') + yield self.build_action(self.req._('view history'), wfurl) + + +# workflow entity types views ################################################## class CellView(view.EntityView): id = 'cell' @@ -129,32 +164,17 @@ row=row, col=col))) -# workflow images ############################################################# - -class ViewWorkflowAction(action.Action): - id = 'workflow' - __select__ = implements('CWEType') & has_related_entities('workflow_of', 'object') +class WorkflowPrimaryView(primary.PrimaryView): + __select__ = implements('Workflow') - category = 'mainactions' - title = _('view workflow') - def url(self): - entity = self.rset.get_entity(self.row or 0, self.col or 0) - return entity.absolute_url(vid='workflow') + def render_entity_attributes(self, entity): + self.w(entity.view('reledit', rtype='description')) + self.w(u'%s' % ( + xml_escape(entity.absolute_url(vid='wfgraph')), + xml_escape(self.req._('graphical workflow for %s') % entity.name))) -class CWETypeWorkflowView(view.EntityView): - id = 'workflow' - __select__ = implements('CWEType') - cache_max_age = 60*60*2 # stay in http cache for 2 hours by default - - def cell_call(self, row, col, **kwargs): - entity = self.rset.get_entity(row, col) - self.w(u'

    %s

    ' % (self.req._('workflow for %s') - % display_name(self.req, entity.name))) - self.w(u'%s' % ( - xml_escape(entity.absolute_url(vid='ewfgraph')), - xml_escape(self.req._('graphical workflow for %s') % entity.name))) - +# workflow images ############################################################## class WorkflowDotPropsHandler(object): def __init__(self, req): @@ -178,7 +198,9 @@ self._('groups:'), ','.join(g.name for g in tr.require_group))) if tr.condition: - descr.append('%s %s'% (self._('condition:'), tr.condition)) + descr.append('%s %s'% ( + self._('condition:'), + ' | '.join(e.expression for e in tr.condition))) if descr: props['label'] += escape('\n'.join(descr)) return props @@ -208,10 +230,10 @@ yield transition.eid, transition.destination().eid, transition -class CWETypeWorkflowImageView(TmpFileViewMixin, view.EntityView): - id = 'ewfgraph' +class WorkflowImageView(TmpFileViewMixin, view.EntityView): + id = 'wfgraph' content_type = 'image/png' - __select__ = implements('CWEType') + __select__ = implements('Workflow') def _generate(self, tmpfile): """display schema information for an entity""" diff -r fc63b80ec979 -r 1d25e928c299 web/webconfig.py --- a/web/webconfig.py Mon Sep 14 11:25:56 2009 +0200 +++ b/web/webconfig.py Tue Sep 15 15:01:41 2009 +0200 @@ -160,18 +160,6 @@ if you want to allow everything', 'group': 'web', 'inputlevel': 1, }), - ('submit-url', - {'type' : 'string', - 'default': Method('default_submit_url'), - 'help': ('URL that may be used to report bug in this instance ' - 'by direct access to the project\'s (jpl) tracker, ' - 'if you want this feature on. The url should looks like ' - 'http://mytracker.com/view?__linkto=concerns:1234:subject&etype=Ticket&type=bug&vid=creation ' - 'where 1234 should be replaced by the eid of your project in ' - 'the tracker. If you have no idea about what I\'am talking ' - 'about, you should probably let no value for this option.'), - 'group': 'web', 'inputlevel': 2, - }), ('submit-mail', {'type' : 'string', 'default': None, @@ -196,16 +184,6 @@ }), )) - def default_submit_url(self): - try: - cube = self.cubes()[0] - cubeeid = self.cube_pkginfo(cube).cube_eid - except Exception: - return None - if cubeeid: - return 'http://intranet.logilab.fr/jpl/view?__linkto=concerns:%s:subject&etype=Ticket&type=bug&vid=creation' % cubeeid - return None - def fckeditor_installed(self): return exists(self.ext_resources['FCKEDITOR_PATH'])