# HG changeset patch # User Sylvain Thénault # Date 1251290756 -7200 # Node ID 7864fee8b4ec0aa1c0520da875beef889a0c710b # Parent 0e346034102304de36b1b3a814a42bd6df202eb9# Parent 238ad682bcb7f187f09fd6bea9088db8989f7c54 backport 3.5 step 1, remaining wf changes in hooks to merge diff -r 0e3460341023 -r 7864fee8b4ec common/mixins.py --- a/common/mixins.py Fri Aug 21 16:26:20 2009 +0200 +++ b/common/mixins.py Wed Aug 26 14:45:56 2009 +0200 @@ -251,7 +251,7 @@ """a recursive path view""" id = 'path' item_vid = 'oneline' - separator = u' > ' + separator = u' > ' def call(self, **kwargs): self.w(u'
') diff -r 0e3460341023 -r 7864fee8b4ec common/test/unittest_uilib.py --- a/common/test/unittest_uilib.py Fri Aug 21 16:26:20 2009 +0200 +++ b/common/test/unittest_uilib.py Wed Aug 26 14:45:56 2009 +0200 @@ -81,8 +81,6 @@ got = uilib.text_cut(text, 30) self.assertEquals(got, expected) - - if __name__ == '__main__': unittest_main() diff -r 0e3460341023 -r 7864fee8b4ec common/uilib.py --- a/common/uilib.py Fri Aug 21 16:26:20 2009 +0200 +++ b/common/uilib.py Wed Aug 26 14:45:56 2009 +0200 @@ -263,7 +263,6 @@ res = unicode(res, 'UTF8') return res - # traceback formatting ######################################################## import traceback @@ -309,7 +308,7 @@ xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2]))) if stackentry[3]: string = xml_escape(stackentry[3]).decode('utf-8', 'replace') - strings.append(u'  %s
\n' % (string)) + strings.append(u'  %s
\n' % (string)) # add locals info for each entry try: local_context = tcbk.tb_frame.f_locals diff -r 0e3460341023 -r 7864fee8b4ec debian/control --- a/debian/control Fri Aug 21 16:26:20 2009 +0200 +++ b/debian/control Wed Aug 26 14:45:56 2009 +0200 @@ -76,7 +76,7 @@ Package: cubicweb-common Architecture: all XB-Python-Version: ${python:Versions} -Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.24.0), python-rql (>= 0.22.1), python-lxml +Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.24.0), python-rql (>= 0.22.3), python-lxml Recommends: python-simpletal (>= 4.0) Conflicts: cubicweb-core Replaces: cubicweb-core diff -r 0e3460341023 -r 7864fee8b4ec debian/cubicweb-ctl.cubicweb.init --- a/debian/cubicweb-ctl.cubicweb.init Fri Aug 21 16:26:20 2009 +0200 +++ b/debian/cubicweb-ctl.cubicweb.init Wed Aug 26 14:45:56 2009 +0200 @@ -22,11 +22,14 @@ # Check if we are sure to not want the start-stop-daemon machinery here # Refer to Debian Policy Manual section 9.3.2 (Writing the scripts) for details. -case "$1" in - "force-reload") - /usr/bin/cubicweb-ctl reload --force - ;; - "*|restart") - /usr/bin/cubicweb-ctl $1 --force - ;; +case $1 in + force-reload) + /usr/bin/cubicweb-ctl reload --force + ;; + status) + /usr/bin/cubicweb-ctl status + ;; + *) + /usr/bin/cubicweb-ctl $1 --force + ;; esac diff -r 0e3460341023 -r 7864fee8b4ec devtools/dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/dataimport.py Wed Aug 26 14:45:56 2009 +0200 @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +"""This module provides tools to import tabular data. + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses + + +Example of use (run this with `cubicweb-ctl shell instance import-script.py`): + +.. sourcecode:: python + + # define data generators + GENERATORS = [] + + USERS = [('Prenom', 'firstname', ()), + ('Nom', 'surname', ()), + ('Identifiant', 'login', ()), + ] + + def gen_users(ctl): + for row in ctl.get_data('utilisateurs'): + entity = mk_entity(row, USERS) + entity['upassword'] = u'motdepasse' + ctl.check('login', entity['login'], None) + ctl.store.add('CWUser', entity) + email = {'address': row['email']} + ctl.store.add('EmailAddress', email) + ctl.store.relate(entity['eid'], 'use_email', email['eid']) + ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']}) + + CHK = [('login', check_doubles, 'Utilisateurs Login', + 'Deux utilisateurs ne devraient pas avoir le même login.'), + ] + + GENERATORS.append( (gen_users, CHK) ) + + # progress callback + def tell(msg): + print msg + + # create controller + ctl = CWImportController(RQLObjectStore()) + ctl.askerror = True + ctl._tell = tell + ctl.generators = GENERATORS + ctl.store._checkpoint = checkpoint + ctl.store._rql = rql + ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv'))) + # run + ctl.run() + sys.exit(0) + +""" +__docformat__ = "restructuredtext en" + +import sys, csv, traceback + +from logilab.common import shellutils + +def utf8csvreader(file, encoding='utf-8', separator=',', quote='"'): + """A csv reader that accepts files with any encoding and outputs + unicode strings.""" + for row in csv.reader(file, delimiter=separator, quotechar=quote): + yield [item.decode(encoding) for item in row] + +def lazytable(reader): + """The first row is taken to be the header of the table and + used to output a dict for each row of data. + + >>> data = lazytable(utf8csvreader(open(filename))) + """ + header = reader.next() + for row in reader: + yield dict(zip(header, row)) + +# base sanitizing functions ##### + +def capitalize_if_unicase(txt): + if txt.isupper() or txt.islower(): + return txt.capitalize() + return txt + +def no_space(txt): + return txt.replace(' ','') + +def no_uspace(txt): + return txt.replace(u'\xa0','') + +def no_dash(txt): + return txt.replace('-','') + +def alldigits(txt): + if txt.isdigit(): + return txt + else: + return u'' + +def strip(txt): + return txt.strip() + +# base checks ##### + +def check_doubles(buckets): + """Extract the keys that have more than one item in their bucket.""" + return [(key, len(value)) for key,value in buckets.items() if len(value) > 1] + +# make entity helper ##### + +def mk_entity(row, map): + """Return a dict made from sanitized mapped values. + + >>> row = {'myname': u'dupont'} + >>> map = [('myname', u'name', (capitalize_if_unicase,))] + >>> mk_entity(row, map) + {'name': u'Dupont'} + """ + res = {} + for src, dest, funcs in map: + res[dest] = row[src] + for func in funcs: + res[dest] = func(res[dest]) + return res + +# object stores + +class ObjectStore(object): + """Store objects in memory for faster testing. Will not + enforce the constraints of the schema and hence will miss + some problems. + + >>> store = ObjectStore() + >>> user = {'login': 'johndoe'} + >>> store.add('CWUser', user) + >>> group = {'name': 'unknown'} + >>> store.add('CWUser', group) + >>> store.relate(user['eid'], 'in_group', group['eid']) + """ + + def __init__(self): + self.items = [] + self.eids = {} + self.types = {} + self.relations = set() + self.indexes = {} + self._rql = None + self._checkpoint = None + + def _put(self, type, item): + self.items.append(item) + return len(self.items) - 1 + + def add(self, type, item): + assert isinstance(item, dict), item + eid = item['eid'] = self._put(type, item) + self.eids[eid] = item + self.types.setdefault(type, []).append(eid) + + def relate(self, eid_from, rtype, eid_to): + eids_valid = (eid_from < len(self.items) and eid_to <= len(self.items)) + assert eids_valid, 'eid error %s %s' % (eid_from, eid_to) + self.relations.add( (eid_from, rtype, eid_to) ) + + def build_index(self, name, type, func): + index = {} + for eid in self.types[type]: + index.setdefault(func(self.eids[eid]), []).append(eid) + self.indexes[name] = index + + def get_many(self, name, key): + return self.indexes[name].get(key, []) + + def get_one(self, name, key): + eids = self.indexes[name].get(key, []) + assert len(eids) == 1 + return eids[0] + + def find(self, type, key, value): + for idx in self.types[type]: + item = self.items[idx] + if item[key] == value: + yield item + + def rql(self, query, args): + if self._rql: + return self._rql(query, args) + + def checkpoint(self): + if self._checkpoint: + self._checkpoint() + +class RQLObjectStore(ObjectStore): + """ObjectStore that works with an actual RQL repository.""" + + def _put(self, type, item): + query = ('INSERT %s X: ' % type) + ', '.join(['X %s %%(%s)s' % (key,key) for key in item]) + return self.rql(query, item)[0][0] + + def relate(self, eid_from, rtype, eid_to): + query = 'SET X %s Y WHERE X eid %%(from)s, Y eid %%(to)s' % rtype + self.rql(query, {'from': int(eid_from), 'to': int(eid_to)}) + self.relations.add( (eid_from, rtype, eid_to) ) + +# import controller ##### + +class CWImportController(object): + """Controller of the data import process. + + >>> ctl = CWImportController(store) + >>> ctl.generators = list_of_data_generators + >>> ctl.data = dict_of_data_tables + >>> ctl.run() + """ + + def __init__(self, store): + self.store = store + self.generators = None + self.data = {} + self.errors = None + self.askerror = False + + def check(self, type, key, value): + self._checks.setdefault(type, {}).setdefault(key, []).append(value) + + def check_map(self, entity, key, map, default): + try: + entity[key] = map[entity[key]] + except KeyError: + self.check(key, entity[key], None) + entity[key] = default + + def run(self): + self.errors = {} + for func, checks in self.generators: + self._checks = {} + func_name = func.__name__[4:] + question = 'Importation de %s' % func_name + self.tell(question) + try: + func(self) + except: + import StringIO + tmp = StringIO.StringIO() + traceback.print_exc(file=tmp) + print tmp.getvalue() + self.errors[func_name] = ('Erreur lors de la transformation', + tmp.getvalue().splitlines()) + for key, func, title, help in checks: + buckets = self._checks.get(key) + if buckets: + err = func(buckets) + if err: + self.errors[title] = (help, err) + self.store.checkpoint() + errors = sum(len(err[1]) for err in self.errors.values()) + self.tell('Importation terminée. (%i objets, %i types, %i relations et %i erreurs).' + % (len(self.store.eids), len(self.store.types), + len(self.store.relations), errors)) + if self.errors and self.askerror and confirm('Afficher les erreurs ?'): + import pprint + pprint.pprint(self.errors) + + def get_data(self, key): + return self.data.get(key) + + def index(self, name, key, value): + self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value) + + def tell(self, msg): + self._tell(msg) + +def confirm(question): + """A confirm function that asks for yes/no/abort and exits on abort.""" + answer = shellutils.ASK.ask(question, ('Y','n','abort'), 'Y') + if answer == 'abort': + sys.exit(1) + return answer == 'Y' diff -r 0e3460341023 -r 7864fee8b4ec entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Fri Aug 21 16:26:20 2009 +0200 +++ b/entities/test/unittest_wfobjs.py Wed Aug 26 14:45:56 2009 +0200 @@ -1,12 +1,15 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb import ValidationError -def add_wf(self, etype, name=None): +def add_wf(self, etype, name=None, default=False): if name is None: - name = unicode(etype) - wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': name}).get_entity(0, 0) + name = etype + wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(name)}).get_entity(0, 0) self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s', {'wf': wf.eid, 'et': etype}) + if default: + self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s', + {'wf': wf.eid, 'et': etype}) return wf def parse_hist(wfhist): @@ -31,10 +34,15 @@ def test_duplicated_state(self): wf = add_wf(self, 'Company') wf.add_state(u'foo', initial=True) + self.commit() wf.add_state(u'foo') ex = self.assertRaises(ValidationError, self.commit) # XXX enhance message self.assertEquals(ex.errors, {'state_of': 'unique constraint S name N, Y state_of O, Y name N failed'}) + # no pb if not in the same workflow + wf2 = add_wf(self, 'Company') + foo = wf2.add_state(u'foo', initial=True) + self.commit() def test_duplicated_transition(self): wf = add_wf(self, 'Company') @@ -98,9 +106,20 @@ trinfo = self._test_manager_deactivate(user) self.assertEquals(trinfo.transition, None) + def test_set_in_state_bad_wf(self): + wf = add_wf(self, 'CWUser') + s = wf.add_state(u'foo', initial=True) + self.commit() + ex = self.assertRaises(ValidationError, self.session().unsafe_execute, + 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s', + {'x': self.user().eid, 's': s.eid}, 'x') + self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. " + "You may want to set a custom workflow for this entity first."}) + def test_fire_transition(self): user = self.user() user.fire_transition('deactivate', comment=u'deactivate user') + user.clear_all_caches() self.assertEquals(user.state, 'deactivated') self._test_manager_deactivate(user) trinfo = self._test_manager_deactivate(user) @@ -139,64 +158,102 @@ 'WHERE T name "deactivate"') self._test_stduser_deactivate() - def _init_wf_with_shared_state_or_tr(self): - req = self.request() - etypes = dict(self.execute('Any N, ET WHERE ET is CWEType, ET name N' - ', ET name IN ("CWGroup", "Bookmark")')) - self.grpwf = req.create_entity('Workflow', ('workflow_of', 'ET'), - ET=etypes['CWGroup'], name=u'group workflow') - self.bmkwf = req.execute('Any X WHERE X is Workflow, X workflow_of ET, ET name "Bookmark"').get_entity(0, 0) - self.state1 = self.grpwf.add_state(u'state1', initial=True) - self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', - {'s': self.state1.eid, 'wf': self.bmkwf.eid}) - self.execute('SET WF initial_state S WHERE S eid %(s)s, WF eid %(wf)s', - {'s': self.state1.eid, 'wf': self.bmkwf.eid}) - self.state2 = self.grpwf.add_state(u'state2') - self.group = self.add_entity('CWGroup', name=u't1') - self.bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view') - # commit to link to the initial state - self.commit() + def test_subworkflow_base(self): + """subworkflow - def test_transitions_selection(self): - """ - ------------------------ tr1 ----------------- - | state1 (CWGroup, Bookmark) | ------> | state2 (CWGroup) | - ------------------------ ----------------- - | tr2 ------------------ - `------> | state3 (Bookmark) | - ------------------ + +-----------+ tr1 +-----------+ + | swfstate1 | ------>| swfstate2 | + +-----------+ +-----------+ + | tr2 +-----------+ + `------>| swfstate3 | + +-----------+ + + main workflow + + +--------+ swftr1 +--------+ + | state1 | -------[swfstate2]->| state2 | + +--------+ | +--------+ + | +--------+ + `-[swfstate3]-->| state3 | + +--------+ """ - self._init_wf_with_shared_state_or_tr() - state3 = self.bmkwf.add_state(u'state3') - tr1 = self.grpwf.add_transition(u'tr1', (self.state1,), self.state2) - tr2 = self.bmkwf.add_transition(u'tr2', (self.state1,), state3) - transitions = list(self.group.possible_transitions()) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') - transitions = list(self.bookmark.possible_transitions()) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - + # sub-workflow + swf = add_wf(self, 'CWGroup', name='subworkflow') + swfstate1 = swf.add_state(u'swfstate1', initial=True) + swfstate2 = swf.add_state(u'swfstate2') + swfstate3 = swf.add_state(u'swfstate3') + tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2) + tr2 = swf.add_transition(u'tr2', (swfstate1,), swfstate3) + # main workflow + mwf = add_wf(self, 'CWGroup', name='main workflow', default=True) + state1 = mwf.add_state(u'state1', initial=True) + state2 = mwf.add_state(u'state2') + state3 = mwf.add_state(u'state3') + swftr1 = mwf.add_wftransition(u'swftr1', swf, state1, + [(swfstate2, state2), (swfstate3, state3)]) + self.assertEquals(swftr1.destination().eid, swfstate1.eid) + # workflows built, begin test + self.group = self.add_entity('CWGroup', name=u'grp1') + self.commit() + self.assertEquals(self.group.current_state.eid, state1.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition(), None) + self.group.fire_transition('swftr1', u'go') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, swfstate1.eid) + self.assertEquals(self.group.current_workflow.eid, swf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid) + self.group.fire_transition('tr1', u'go') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, state2.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertEquals(self.group.subworkflow_input_transition(), None) + # force back to swfstate1 is impossible since we can't any more find + # subworkflow input transition + ex = self.assertRaises(ValidationError, + self.group.change_state, swfstate1, u'gadget') + self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's current workflow"}) + self.rollback() + # force back to state1 + self.group.change_state('state1', u'gadget') + self.group.fire_transition('swftr1', u'au') + self.group.clear_all_caches() + self.group.fire_transition('tr2', u'chapeau') + self.commit() + self.group.clear_all_caches() + self.assertEquals(self.group.current_state.eid, state3.eid) + self.assertEquals(self.group.current_workflow.eid, mwf.eid) + self.assertEquals(self.group.main_workflow.eid, mwf.eid) + self.assertListEquals(parse_hist(self.group.workflow_history), + [('state1', 'swfstate1', 'swftr1', 'go'), + ('swfstate1', 'swfstate2', 'tr1', 'go'), + ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'), + ('state2', 'state1', None, 'gadget'), + ('state1', 'swfstate1', 'swftr1', 'au'), + ('swfstate1', 'swfstate3', 'tr2', 'chapeau'), + ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'), + ]) - def test_transitions_selection2(self): - """ - ------------------------ tr1 (Bookmark) ----------------------- - | state1 (CWGroup, Bookmark) | -------------> | state2 (CWGroup,Bookmark) | - ------------------------ ----------------------- - | tr2 (CWGroup) | - `---------------------------------/ - """ - self._init_wf_with_shared_state_or_tr() - self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', - {'s': self.state2.eid, 'wf': self.bmkwf.eid}) - tr1 = self.bmkwf.add_transition(u'tr1', (self.state1,), self.state2) - tr2 = self.grpwf.add_transition(u'tr2', (self.state1,), self.state2) - transitions = list(self.group.possible_transitions()) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr2') - transitions = list(self.bookmark.possible_transitions()) - self.assertEquals(len(transitions), 1) - self.assertEquals(transitions[0].name, 'tr1') + def test_subworkflow_exit_consistency(self): + # sub-workflow + swf = add_wf(self, 'CWGroup', name='subworkflow') + swfstate1 = swf.add_state(u'swfstate1', initial=True) + swfstate2 = swf.add_state(u'swfstate2') + tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2) + # main workflow + mwf = add_wf(self, 'CWGroup', name='main workflow', default=True) + state1 = mwf.add_state(u'state1', initial=True) + state2 = mwf.add_state(u'state2') + state3 = mwf.add_state(u'state3') + mwf.add_wftransition(u'swftr1', swf, state1, + [(swfstate2, state2), (swfstate2, state3)]) + ex = self.assertRaises(ValidationError, self.commit) + self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"}) class CustomWorkflowTC(CubicWebTC): @@ -239,25 +296,6 @@ ('deactivated', 'activated', 'activate', None), ('activated', 'asleep', None, 'workflow changed to "CWUser"')]) - def test_custom_wf_shared_state(self): - """member in some state shared by the new workflow, nothing has to be - done - """ - self.member.fire_transition('deactivate') - self.assertEquals(self.member.state, 'deactivated') - wf = add_wf(self, 'CWUser') - wf.add_state('asleep', initial=True) - self.execute('SET S state_of WF WHERE S name "deactivated", WF eid %(wf)s', - {'wf': wf.eid}) - self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', - {'wf': wf.eid, 'x': self.member.eid}) - self.commit() - self.member.clear_all_caches() - self.assertEquals(self.member.current_workflow.eid, wf.eid) - self.assertEquals(self.member.state, 'deactivated') - self.assertEquals(parse_hist(self.member.workflow_history), - [('activated', 'deactivated', 'deactivate', None)]) - def test_custom_wf_no_initial_state(self): """try to set a custom workflow which has no initial state""" self.member.fire_transition('deactivate') @@ -269,8 +307,7 @@ self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'}) def test_custom_wf_bad_etype(self): - """try to set a custom workflow which has no initial state""" - self.member.fire_transition('deactivate') + """try to set a custom workflow which doesn't apply to entity type""" wf = add_wf(self, 'Company') wf.add_state('asleep', initial=True) self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', diff -r 0e3460341023 -r 7864fee8b4ec entities/wfobjs.py --- a/entities/wfobjs.py Fri Aug 21 16:26:20 2009 +0200 +++ b/entities/wfobjs.py Wed Aug 26 14:45:56 2009 +0200 @@ -16,6 +16,7 @@ from cubicweb.interfaces import IWorkflowable from cubicweb.common.mixins import MI_REL_TRIGGERS +class WorkflowException(Exception): pass class Workflow(AnyEntity): id = 'Workflow' @@ -40,6 +41,23 @@ return self.workflow_of[0].rest_path(), {'vid': 'workflow'} return super(Workflow, self).after_deletion_path() + def iter_workflows(self, _done=None): + """return an iterator on actual workflows, eg this workflow and its + subworkflows + """ + # infinite loop safety belt + if _done is None: + _done = set() + yield self + _done.add(self.eid) + for tr in self.req.execute('Any T WHERE T is WorkflowTransition, ' + 'T transition_of WF, WF eid %(wf)s', + {'wf': self.eid}).entities(): + if tr.subwf.eid in _done: + continue + for subwf in tr.subwf.iter_workflows(_done): + yield subwf + # state / transitions accessors ############################################ def state_by_name(self, statename): @@ -77,9 +95,7 @@ # wf construction methods ################################################## def add_state(self, name, initial=False, **kwargs): - """method to ease workflow definition: add a state for one or more - entity type(s) - """ + """add a state to this workflow""" state = self.req.create_entity('State', name=unicode(name), **kwargs) self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s', {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) @@ -90,27 +106,47 @@ {'s': state.eid, 'wf': self.eid}, ('s', 'wf')) return state - def add_transition(self, name, fromstates, tostate, - requiredgroups=(), conditions=(), **kwargs): - """method to ease workflow definition: add a transition for one or more - entity type(s), from one or more state and to a single state - """ - tr = self.req.create_entity('Transition', name=unicode(name), **kwargs) + def _add_transition(self, trtype, name, fromstates, + requiredgroups=(), conditions=(), **kwargs): + tr = self.req.create_entity(trtype, name=unicode(name), **kwargs) self.req.execute('SET T transition_of WF ' 'WHERE T eid %(t)s, WF eid %(wf)s', {'t': tr.eid, 'wf': self.eid}, ('t', 'wf')) + assert fromstates, fromstates + if not isinstance(fromstates, (tuple, list)): + fromstates = (fromstates,) for state in fromstates: if hasattr(state, 'eid'): state = state.eid self.req.execute('SET S allowed_transition T ' 'WHERE S eid %(s)s, T eid %(t)s', {'s': state, 't': tr.eid}, ('s', 't')) + tr.set_transition_permissions(requiredgroups, conditions, reset=False) + return tr + + def add_transition(self, name, fromstates, tostate, + requiredgroups=(), conditions=(), **kwargs): + """add a transition to this workflow from some state(s) to another""" + tr = self._add_transition('Transition', name, fromstates, + requiredgroups, conditions, **kwargs) if hasattr(tostate, 'eid'): tostate = tostate.eid self.req.execute('SET T destination_state S ' 'WHERE S eid %(s)s, T eid %(t)s', {'t': tr.eid, 's': tostate}, ('s', 't')) - tr.set_transition_permissions(requiredgroups, conditions, reset=False) + return tr + + def add_wftransition(self, name, subworkflow, fromstates, exitpoints, + requiredgroups=(), conditions=(), **kwargs): + """add a workflow transition to this workflow""" + tr = self._add_transition('WorkflowTransition', name, fromstates, + requiredgroups, conditions, **kwargs) + if hasattr(subworkflow, 'eid'): + subworkflow = subworkflow.eid + self.req.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s', + {'t': tr.eid, 'wf': subworkflow}, ('wf', 't')) + for fromstate, tostate in exitpoints: + tr.add_exit_point(fromstate, tostate) return tr @@ -125,9 +161,18 @@ def __init__(self, *args, **kwargs): if self.id == 'BaseTransition': - raise Exception('should not be instantiated') + raise WorkflowException('should not be instantiated') super(BaseTransition, self).__init__(*args, **kwargs) + @property + def workflow(self): + return self.transition_of[0] + + def has_input_state(self, state): + if hasattr(state, 'eid'): + state = state.eid + return any(s for s in self.reverse_allowed_transition if s.eid == state) + def may_be_fired(self, eid): """return true if the logged user may fire this transition @@ -170,7 +215,6 @@ self.req.execute('DELETE T condition R WHERE T eid %(x)s', {'x': self.eid}, 'x') for gname in requiredgroups: - ### XXX ensure gname validity rset = self.req.execute('SET T require_group G ' 'WHERE T eid %(x)s, G name %(gn)s', {'x': self.eid, 'gn': gname}, 'x') @@ -194,11 +238,6 @@ def destination(self): return self.destination_state[0] - def has_input_state(self, state): - if hasattr(state, 'eid'): - state = state.eid - return any(s for s in self.reverse_allowed_transition if s.eid == state) - class WorkflowTransition(BaseTransition): """customized class for WorkflowTransition entities""" @@ -211,6 +250,50 @@ def destination(self): return self.subwf.initial + def add_exit_point(self, fromstate, tostate): + if hasattr(fromstate, 'eid'): + fromstate = fromstate.eid + if hasattr(tostate, 'eid'): + tostate = tostate.eid + self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, ' + 'X subworkflow_state FS, X destination_state TS ' + 'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s', + {'t': self.eid, 'fs': fromstate, 'ts': tostate}, + ('t', 'fs', 'ts')) + + def get_exit_point(self, state): + """if state is an exit point, return its associated destination state""" + if hasattr(state, 'eid'): + state = state.eid + stateeid = self.exit_points().get(state) + if stateeid is not None: + return self.req.entity_from_eid(stateeid) + return None + + @cached + def exit_points(self): + result = {} + for ep in self.subworkflow_exit: + result[ep.subwf_state.eid] = ep.destination.eid + return result + + def clear_all_caches(self): + super(WorkflowableMixIn, self).clear_all_caches() + clear_cache(self, 'exit_points') + + +class SubWorkflowExitPoint(AnyEntity): + """customized class for SubWorkflowExitPoint entities""" + id = 'SubWorkflowExitPoint' + + @property + def subwf_state(self): + return self.subworkflow_state[0] + + @property + def destination(self): + return self.destination_state[0] + class State(AnyEntity): """customized class for State entities""" @@ -218,6 +301,10 @@ fetch_attrs, fetch_order = fetch_config(['name']) rest_attr = 'eid' + @property + def workflow(self): + return self.state_of[0] + def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -266,13 +353,18 @@ __implements__ = (IWorkflowable,) @property - def current_workflow(self): + def main_workflow(self): """return current workflow applied to this entity""" if self.custom_workflow: return self.custom_workflow[0] return self.cwetype_workflow() @property + def current_workflow(self): + """return current workflow applied to this entity""" + return self.current_state and self.current_state.workflow or self.main_workflow + + @property def current_state(self): """return current state entity""" return self.in_state and self.in_state[0] or None @@ -337,13 +429,21 @@ if tr.may_be_fired(self.eid): yield tr - def _get_tr_kwargs(self, comment, commentformat): + def _add_trinfo(self, comment, commentformat, treid=None, tseid=None): kwargs = {} if comment is not None: kwargs['comment'] = comment if commentformat is not None: kwargs['comment_format'] = commentformat - return kwargs + args = [('wf_info_for', 'E')] + kwargs['E'] = self.eid + if treid is not None: + args.append( ('by_transition', 'T') ) + kwargs['T'] = treid + if tseid is not None: + args.append( ('to_state', 'S') ) + kwargs['S'] = tseid + return self.req.create_entity('TrInfo', *args, **kwargs) def fire_transition(self, trname, comment=None, commentformat=None): """change the entity's state by firing transition of the given name in @@ -351,30 +451,51 @@ """ assert self.current_workflow tr = self.current_workflow.transition_by_name(trname) - assert tr is not None, 'not a %s transition: %s' % (self.id, state) - # XXX try to find matching transition? - self.req.create_entity('TrInfo', ('by_transition', 'T'), - ('wf_info_for', 'E'), T=tr.eid, E=self.eid, - **self._get_tr_kwargs(comment, commentformat)) + assert tr is not None, 'not a %s transition: %s' % (self.id, trname) + return self._add_trinfo(comment, commentformat, tr.eid) - def change_state(self, statename, comment=None, commentformat=None): - """change the entity's state to the state of the given name in entity's - workflow. This method should only by used by manager to fix an entity's - state when their is no matching transition, otherwise fire_transition - should be used. + def change_state(self, statename, comment=None, commentformat=None, tr=None): + """change the entity's state to the given state (name or entity) in + entity's workflow. This method should only by used by manager to fix an + entity's state when their is no matching transition, otherwise + fire_transition should be used. """ assert self.current_workflow - if not isinstance(statename, basestring): - warn('give a state name') - state = self.current_workflow.state_by_eid(statename) - assert state is not None, 'not a %s state: %s' % (self.id, state) + if hasattr(statename, 'eid'): + stateeid = statename.eid else: - state = self.current_workflow.state_by_name(statename) + if not isinstance(statename, basestring): + warn('give a state name') + state = self.current_workflow.state_by_eid(statename) + else: + state = self.current_workflow.state_by_name(statename) + if state is None: + raise WorkflowException('not a %s state: %s' % (self.id, + statename)) + stateeid = state.eid # XXX try to find matching transition? - self.req.create_entity('TrInfo', ('to_state', 'S'), - ('wf_info_for', 'E'), S=state.eid, E=self.eid, - **self._get_tr_kwargs(comment, commentformat)) + return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid) + def subworkflow_input_transition(self): + """return the transition which has went through the current sub-workflow + """ + if self.main_workflow.eid == self.current_workflow.eid: + return # doesn't make sense + subwfentries = [] + for trinfo in reversed(self.workflow_history): + if (trinfo.transition and + trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid): + # entering or leaving a subworkflow + if (subwfentries and + subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid): + # leave + del subwfentries[-1] + else: + # enter + subwfentries.append(trinfo) + if not subwfentries: + return None + return subwfentries[-1].transition def clear_all_caches(self): super(WorkflowableMixIn, self).clear_all_caches() diff -r 0e3460341023 -r 7864fee8b4ec entity.py --- a/entity.py Fri Aug 21 16:26:20 2009 +0200 +++ b/entity.py Wed Aug 26 14:45:56 2009 +0200 @@ -209,6 +209,9 @@ def __hash__(self): return id(self) + def __cmp__(self): + raise NotImplementedError('comparison not implemented for %s' % self.__class__) + def pre_add_hook(self): """hook called by the repository before doing anything to add the entity (before_add entity hooks have not been called yet). This give the diff -r 0e3460341023 -r 7864fee8b4ec goa/appobjects/components.py --- a/goa/appobjects/components.py Fri Aug 21 16:26:20 2009 +0200 +++ b/goa/appobjects/components.py Wed Aug 26 14:45:56 2009 +0200 @@ -74,7 +74,7 @@ label = display_name(req, etype, 'plural') view = self.vreg.select('views', 'list', req, req.etype_rset(etype)) url = view.url() - etypelink = u' %s' % (xml_escape(url), label) + etypelink = u' %s' % (xml_escape(url), label) yield (label, etypelink, self.add_entity_link(eschema, req)) ManageView.entity_types = entity_types_no_count diff -r 0e3460341023 -r 7864fee8b4ec goa/appobjects/gauthservice.py --- a/goa/appobjects/gauthservice.py Fri Aug 21 16:26:20 2009 +0200 +++ b/goa/appobjects/gauthservice.py Wed Aug 26 14:45:56 2009 +0200 @@ -17,7 +17,7 @@ def anon_user_link(self): self.w(self.req._('anonymous')) - self.w(u' [%s]' + self.w(u' [%s]' % (users.create_login_url(self.req.url()), self.req._('login'))) class GAELogoutAction(LogoutAction): diff -r 0e3460341023 -r 7864fee8b4ec hooks/notification.py --- a/hooks/notification.py Fri Aug 21 16:26:20 2009 +0200 +++ b/hooks/notification.py Wed Aug 26 14:45:56 2009 +0200 @@ -47,9 +47,10 @@ if view is None: return comment = entity.printable_value('comment', format='text/plain') - if comment: - comment = normalize_text(comment, 80, - rest=entity.comment_format=='text/rest') + # XXX don't try to wrap rest until we've a proper transformation (see + # #103822) + if comment and entity.comment_format != 'text/rest': + comment = normalize_text(comment, 80) RenderAndSendNotificationView(self._cw, view=view, viewargs={ 'comment': comment, 'previous_state': entity.previous_state.name, 'current_state': entity.new_state.name}) diff -r 0e3460341023 -r 7864fee8b4ec i18n/en.po --- a/i18n/en.po Fri Aug 21 16:26:20 2009 +0200 +++ b/i18n/en.po Wed Aug 26 14:45:56 2009 +0200 @@ -75,31 +75,31 @@ msgstr "" #, python-format -msgid "%d days" +msgid "%d days" msgstr "" #, python-format -msgid "%d hours" +msgid "%d hours" msgstr "" #, python-format -msgid "%d minutes" +msgid "%d minutes" msgstr "" #, python-format -msgid "%d months" +msgid "%d months" msgstr "" #, python-format -msgid "%d seconds" +msgid "%d seconds" msgstr "" #, python-format -msgid "%d weeks" +msgid "%d weeks" msgstr "" #, python-format -msgid "%d years" +msgid "%d years" msgstr "" #, python-format diff -r 0e3460341023 -r 7864fee8b4ec i18n/es.po --- a/i18n/es.po Fri Aug 21 16:26:20 2009 +0200 +++ b/i18n/es.po Wed Aug 26 14:45:56 2009 +0200 @@ -80,32 +80,32 @@ msgstr "%d años" #, python-format -msgid "%d days" -msgstr "%d días" +msgid "%d days" +msgstr "%d días" #, python-format -msgid "%d hours" -msgstr "%d horas" +msgid "%d hours" +msgstr "%d horas" #, python-format -msgid "%d minutes" -msgstr "%d minutos" +msgid "%d minutes" +msgstr "%d minutos" #, python-format -msgid "%d months" -msgstr "%d meses" +msgid "%d months" +msgstr "%d meses" #, python-format -msgid "%d seconds" -msgstr "%d segundos" +msgid "%d seconds" +msgstr "%d segundos" #, python-format -msgid "%d weeks" -msgstr "%d semanas" +msgid "%d weeks" +msgstr "%d semanas" #, python-format -msgid "%d years" -msgstr "%d años" +msgid "%d years" +msgstr "%d años" #, python-format msgid "%s error report" diff -r 0e3460341023 -r 7864fee8b4ec i18n/fr.po --- a/i18n/fr.po Fri Aug 21 16:26:20 2009 +0200 +++ b/i18n/fr.po Wed Aug 26 14:45:56 2009 +0200 @@ -80,32 +80,32 @@ msgstr "%d années" #, python-format -msgid "%d days" -msgstr "%d jours" +msgid "%d days" +msgstr "%d jours" #, python-format -msgid "%d hours" -msgstr "%d heures" +msgid "%d hours" +msgstr "%d heures" #, python-format -msgid "%d minutes" -msgstr "%d minutes" +msgid "%d minutes" +msgstr "%d minutes" #, python-format -msgid "%d months" -msgstr "%d mois" +msgid "%d months" +msgstr "%d mois" #, python-format -msgid "%d seconds" -msgstr "%d secondes" +msgid "%d seconds" +msgstr "%d secondes" #, python-format -msgid "%d weeks" -msgstr "%d semaines" +msgid "%d weeks" +msgstr "%d semaines" #, python-format -msgid "%d years" -msgstr "%d années" +msgid "%d years" +msgstr "%d années" #, python-format msgid "%s error report" diff -r 0e3460341023 -r 7864fee8b4ec misc/migration/postcreate.py --- a/misc/migration/postcreate.py Fri Aug 21 16:26:20 2009 +0200 +++ b/misc/migration/postcreate.py Wed Aug 26 14:45:56 2009 +0200 @@ -6,14 +6,30 @@ :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ -activatedeid = add_state(_('activated'), 'CWUser', initial=True) -deactivatedeid = add_state(_('deactivated'), 'CWUser') -add_transition(_('deactivate'), 'CWUser', - (activatedeid,), deactivatedeid, - requiredgroups=('managers',)) -add_transition(_('activate'), 'CWUser', - (deactivatedeid,), activatedeid, - requiredgroups=('managers',)) +# insert versions +create_entity('CWProperty', pkey=u'system.version.cubicweb', + value=unicode(config.cubicweb_version())) +for cube in config.cubes(): + create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(), + value=unicode(config.cube_version(cube))) + +# some entities have been added before schema entities, fix the 'is' and +# 'is_instance_of' relations +for rtype in ('is', 'is_instance_of'): + sql('INSERT INTO %s_relation ' + 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' + 'WHERE X.type=ET.cw_name AND NOT EXISTS(' + ' SELECT 1 from is_relation ' + ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) + +# user workflow +userwf = add_workflow(_('default user workflow'), 'CWUser') +activated = userwf.add_state(_('activated'), initial=True) +deactivated = userwf.add_state(_('deactivated')) +userwf.add_transition(_('deactivate'), (activated,), deactivated, + requiredgroups=('managers',)) +userwf.add_transition(_('activate'), (deactivated,), activated, + requiredgroups=('managers',)) # create anonymous user if all-in-one config and anonymous user has been specified if hasattr(config, 'anonymous_user'): @@ -26,23 +42,23 @@ # need this since we already have at least one user in the database (the default admin) for user in rql('Any X WHERE X is CWUser').entities(): session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s', - {'x': user.eid, 's': activatedeid}, 'x') - -cfg = config.persistent_options_configuration() -if interactive_mode: - cfg.input_config(inputlevel=0) + {'x': user.eid, 's': activated.eid}, 'x') -for section, options in cfg.options_by_section(): - for optname, optdict, value in options: - key = '%s.%s' % (section, optname) - default = cfg.option_default(optname, optdict) - # only record values differing from default - if value != default: - rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) +# on interactive mode, ask for level 0 persistent options +if interactive_mode: + cfg = config.persistent_options_configuration() + cfg.input_config(inputlevel=0) + for section, options in cfg.options_by_section(): + for optname, optdict, value in options: + key = '%s.%s' % (section, optname) + default = cfg.option_default(optname, optdict) + # only record values differing from default + if value != default: + rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value}) # add PERM_USE_TEMPLATE_FORMAT permission from cubicweb.schema import PERM_USE_TEMPLATE_FORMAT -eid = add_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, - label=_('use template languages')) +usetmplperm = create_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT, + label=_('use template languages')) rql('SET X require_group G WHERE G name "managers", X eid %(x)s', - {'x': eid}, 'x') + {'x': usetmplperm.eid}, 'x') diff -r 0e3460341023 -r 7864fee8b4ec rset.py --- a/rset.py Fri Aug 21 16:26:20 2009 +0200 +++ b/rset.py Wed Aug 26 14:45:56 2009 +0200 @@ -447,7 +447,7 @@ if rqlst.TYPE == 'select': # UNION query, find the subquery from which this entity has been # found - rqlst = rqlst.locate_subquery(col, etype, self.args) + rqlst, col = rqlst.locate_subquery(col, etype, self.args) # take care, due to outer join support, we may find None # values for non final relation for i, attr, x in attr_desc_iterator(rqlst, col): @@ -547,7 +547,8 @@ if len(self.column_types(i)) > 1: break # UNION query, find the subquery from which this entity has been found - select = rqlst.locate_subquery(locate_query_col, etype, self.args) + select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0] + col = rqlst.subquery_selection_index(select, col) try: myvar = select.selection[col].variable except AttributeError: @@ -555,7 +556,7 @@ return None, None rel = myvar.main_relation() if rel is not None: - index = rel.children[0].variable.selected_index() + index = rel.children[0].root_selection_index() if index is not None and self.rows[row][index]: return self.get_entity(row, index), rel.r_type return None, None diff -r 0e3460341023 -r 7864fee8b4ec schemas/workflow.py --- a/schemas/workflow.py Fri Aug 21 16:26:20 2009 +0200 +++ b/schemas/workflow.py Wed Aug 26 14:45:56 2009 +0200 @@ -58,7 +58,7 @@ allowed_transition = SubjectRelation('BaseTransition', cardinality='**', constraints=[RQLConstraint('S state_of WF, O transition_of WF')], description=_('allowed transitions from this state')) - state_of = SubjectRelation('Workflow', cardinality='+*', + state_of = SubjectRelation('Workflow', cardinality='1*', description=_('workflow to which this state belongs'), constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')]) @@ -81,7 +81,7 @@ require_group = SubjectRelation('CWGroup', cardinality='**', description=_('group in which a user should be to be ' 'allowed to pass this transition')) - transition_of = SubjectRelation('Workflow', cardinality='+*', + transition_of = SubjectRelation('Workflow', cardinality='1*', description=_('workflow to which this transition belongs'), constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')]) diff -r 0e3460341023 -r 7864fee8b4ec schemaviewer.py --- a/schemaviewer.py Fri Aug 21 16:26:20 2009 +0200 +++ b/schemaviewer.py Wed Aug 26 14:45:56 2009 +0200 @@ -104,7 +104,7 @@ """get a layout for an entity schema""" etype = eschema.type layout = Section(children=' ', klass='clear') - layout.append(Link(etype,' ' , id=etype)) # anchor + layout.append(Link(etype,' ' , id=etype)) # anchor title = Link(self.eschema_link_url(eschema), etype) boxchild = [Section(children=(title, ' (%s)'% eschema.display_name(self.req)), klass='title')] table = Table(cols=4, rheaders=1, diff -r 0e3460341023 -r 7864fee8b4ec selectors.py diff -r 0e3460341023 -r 7864fee8b4ec server/__init__.py --- a/server/__init__.py Fri Aug 21 16:26:20 2009 +0200 +++ b/server/__init__.py Wed Aug 26 14:45:56 2009 +0200 @@ -180,22 +180,6 @@ handler = config.migration_handler(schema, interactive=False, cnx=cnx, repo=repo) initialize_schema(config, schema, handler) - # insert versions - handler.cmd_add_entity('CWProperty', pkey=u'system.version.cubicweb', - value=unicode(config.cubicweb_version())) - for cube in config.cubes(): - handler.cmd_add_entity('CWProperty', - pkey=u'system.version.%s' % cube.lower(), - value=unicode(config.cube_version(cube))) - # some entities have been added before schema entities, fix the 'is' and - # 'is_instance_of' relations - for rtype in ('is', 'is_instance_of'): - handler.sqlexec( - 'INSERT INTO %s_relation ' - 'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET ' - 'WHERE X.type=ET.cw_name AND NOT EXISTS(' - ' SELECT 1 from is_relation ' - ' WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype) # yoo ! cnx.commit() config.enabled_sources = None diff -r 0e3460341023 -r 7864fee8b4ec server/migractions.py --- a/server/migractions.py Fri Aug 21 16:26:20 2009 +0200 +++ b/server/migractions.py Wed Aug 26 14:45:56 2009 +0200 @@ -1022,7 +1022,7 @@ if commit: self.commit() - @deprecated('use entity.change_state("state")') + @deprecated('use entity.fire_transition("transition") or entity.change_state("state")') def cmd_set_state(self, eid, statename, commit=False): self.session.set_pool() # ensure pool is set self.session.entity_from_eid(eid).change_state(statename) diff -r 0e3460341023 -r 7864fee8b4ec server/serverctl.py --- a/server/serverctl.py Fri Aug 21 16:26:20 2009 +0200 +++ b/server/serverctl.py Wed Aug 26 14:45:56 2009 +0200 @@ -257,12 +257,19 @@ 'help': 'verbose mode: will ask all possible configuration questions', } ), + ('automatic', + {'short': 'a', 'type' : 'yn', 'metavar': '', + 'default': 'n', + 'help': 'automatic mode: never ask and use default answer to every question', + } + ), ) def run(self, args): """run the command with its specific arguments""" from logilab.common.adbh import get_adv_func_helper from indexer import get_indexer verbose = self.get('verbose') + automatic = self.get('automatic') appid = pop_arg(args, msg='No instance specified !') config = ServerConfiguration.config_for(appid) create_db = self.config.create_db @@ -277,13 +284,13 @@ try: if helper.users_support: user = source['db-user'] - if not helper.user_exists(cursor, user) and \ - ASK.confirm('Create db user %s ?' % user, default_is_yes=False): + if not helper.user_exists(cursor, user) and (automatic or \ + ASK.confirm('Create db user %s ?' % user, default_is_yes=False)): helper.create_user(source['db-user'], source['db-password']) print '-> user %s created.' % user dbname = source['db-name'] if dbname in helper.list_databases(cursor): - if ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname): + if automatic or ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname): cursor.execute('DROP DATABASE %s' % dbname) else: return @@ -311,7 +318,7 @@ cnx.commit() print '-> database for instance %s created and necessary extensions installed.' % appid print - if ASK.confirm('Run db-init to initialize the system database ?'): + if automatic or ASK.confirm('Run db-init to initialize the system database ?'): cmd_run('db-init', config.appid) else: print ('-> nevermind, you can do it later with ' diff -r 0e3460341023 -r 7864fee8b4ec server/sources/pyrorql.py --- a/server/sources/pyrorql.py Fri Aug 21 16:26:20 2009 +0200 +++ b/server/sources/pyrorql.py Wed Aug 26 14:45:56 2009 +0200 @@ -26,6 +26,12 @@ from cubicweb.server.sources import (AbstractSource, ConnectionWrapper, TimedCache, dbg_st_search, dbg_results) + +def uidtype(union, col, etype, args): + select, col = union.locate_subquery(col, etype, args) + return getattr(select.selection[col], 'uidtype', None) + + class ReplaceByInOperator(Exception): def __init__(self, eids): self.eids = eids @@ -295,8 +301,8 @@ needtranslation = [] rows = rset.rows for i, etype in enumerate(descr[0]): - if (etype is None or not self.schema.eschema(etype).is_final() or - getattr(union.locate_subquery(i, etype, args).selection[i], 'uidtype', None)): + if (etype is None or not self.schema.eschema(etype).is_final() + or uidtype(union, i, etype, args)): needtranslation.append(i) if needtranslation: cnx = session.pool.connection(self.uri) diff -r 0e3460341023 -r 7864fee8b4ec sobjects/notification.py --- a/sobjects/notification.py Fri Aug 21 16:26:20 2009 +0200 +++ b/sobjects/notification.py Wed Aug 26 14:45:56 2009 +0200 @@ -104,8 +104,12 @@ entity = self.rset.get_entity(self.row or 0, self.col or 0) content = entity.printable_value(self.content_attr, format='text/plain') if content: - contentformat = getattr(entity, self.content_attr + '_format', 'text/rest') - content = normalize_text(content, 80, rest=contentformat=='text/rest') + contentformat = getattr(entity, self.content_attr + '_format', + 'text/rest') + # XXX don't try to wrap rest until we've a proper transformation (see + # #103822) + if contentformat != 'text/rest': + content = normalize_text(content, 80) return super(ContentAddedView, self).context(content=content, **kwargs) def subject(self): diff -r 0e3460341023 -r 7864fee8b4ec test/unittest_rset.py --- a/test/unittest_rset.py Fri Aug 21 16:26:20 2009 +0200 +++ b/test/unittest_rset.py Wed Aug 26 14:45:56 2009 +0200 @@ -328,6 +328,7 @@ entity, rtype = rset.related_entity(1, 1) self.assertEquals(entity.id, 'CWGroup') self.assertEquals(rtype, 'name') + # rset = self.execute('Any X,N ORDERBY N WHERE X is Bookmark WITH X,N BEING ' '((Any X,N WHERE X is CWGroup, X name N)' ' UNION ' @@ -335,6 +336,14 @@ entity, rtype = rset.related_entity(0, 1) self.assertEquals(entity.eid, e.eid) self.assertEquals(rtype, 'title') + # + rset = self.execute('Any X,N ORDERBY N WITH N,X BEING ' + '((Any N,X WHERE X is CWGroup, X name N)' + ' UNION ' + ' (Any N,X WHERE X is Bookmark, X title N))') + entity, rtype = rset.related_entity(0, 1) + self.assertEquals(entity.eid, e.eid) + self.assertEquals(rtype, 'title') def test_entities(self): rset = self.execute('Any U,G WHERE U in_group G') diff -r 0e3460341023 -r 7864fee8b4ec view.py --- a/view.py Fri Aug 21 16:26:20 2009 +0200 +++ b/view.py Wed Aug 26 14:45:56 2009 +0200 @@ -474,8 +474,11 @@ __registry__ = 'components' __select__ = yes() + # XXX huummm, much probably useless + htmlclass = 'mainRelated' def div_class(self): - return '%s %s' % (self.cw_propval('htmlclass'), self.id) + return '%s %s' % (self.htmlclass, self.id) + # XXX a generic '%s%s' % (self.id, self.__registry__.capitalize()) would probably be nicer def div_id(self): return '%sComponent' % self.id diff -r 0e3460341023 -r 7864fee8b4ec web/component.py --- a/web/component.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/component.py Wed Aug 26 14:45:56 2009 +0200 @@ -8,6 +8,8 @@ __docformat__ = "restructuredtext en" _ = unicode +from simplejson import dumps + from logilab.common.deprecation import class_renamed from logilab.mtconverter import xml_escape @@ -43,8 +45,6 @@ _('navcontenttop'), _('navcontentbottom')), #vocabulary=(_('header'), _('incontext'), _('footer')), help=_('context where this component should be displayed')), - _('htmlclass'):dict(type='String', default='mainRelated', - help=_('html class of the component')), } context = 'navcontentbottom' # 'footer' | 'header' | 'incontext' @@ -72,7 +72,8 @@ page_link_templ = u'%s' selected_page_link_templ = u'%s' previous_page_link_templ = next_page_link_templ = page_link_templ - no_previous_page_link = no_next_page_link = u'' + no_previous_page_link = u'<<' + no_next_page_link = u'>>' def __init__(self, req, rset, **kwargs): super(NavigationComponent, self).__init__(req, rset=rset, **kwargs) @@ -112,33 +113,40 @@ if self.stop_param in params: del params[self.stop_param] + def page_url(self, path, params, start, stop): + params = merge_dicts(params, {self.start_param : start, + self.stop_param : stop,}) + if path == 'json': + rql = params.pop('rql', self.rset.printable_rql()) + # latest 'true' used for 'swap' mode + url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % ( + dumps(params.get('divid', 'paginated-content')), + dumps(rql), dumps(params.pop('vid', None)), dumps(params)) + else: + url = self.build_url(path, **params) + return url + def page_link(self, path, params, start, stop, content): - url = self.build_url(path, **merge_dicts(params, {self.start_param : start, - self.stop_param : stop,})) - url = xml_escape(url) + url = xml_escape(self.page_url(path, params, start, stop)) if start == self.starting_from: return self.selected_page_link_templ % (url, content, content) return self.page_link_templ % (url, content, content) - def previous_link(self, params, content='<<', title=_('previous_results')): + def previous_link(self, path, params, content='<<', title=_('previous_results')): start = self.starting_from if not start : return self.no_previous_page_link start = max(0, start - self.page_size) stop = start + self.page_size - 1 - url = self.build_url(**merge_dicts(params, {self.start_param : start, - self.stop_param : stop,})) - url = xml_escape(url) + url = xml_escape(self.page_url(path, params, start, stop)) return self.previous_page_link_templ % (url, title, content) - def next_link(self, params, content='>>', title=_('next_results')): + def next_link(self, path, params, content='>>', title=_('next_results')): start = self.starting_from + self.page_size if start >= self.total: return self.no_next_page_link stop = start + self.page_size - 1 - url = self.build_url(**merge_dicts(params, {self.start_param : start, - self.stop_param : stop,})) - url = xml_escape(url) + url = xml_escape(self.page_url(path, params, start, stop)) return self.next_page_link_templ % (url, title, content) diff -r 0e3460341023 -r 7864fee8b4ec web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Fri Aug 21 16:26:20 2009 +0200 +++ b/web/data/cubicweb.ajax.js Wed Aug 26 14:45:56 2009 +0200 @@ -66,7 +66,16 @@ jQuery(CubicWeb).trigger('ajax-loaded'); } -// cubicweb loadxhtml plugin to make jquery handle xhtml response +/* cubicweb loadxhtml plugin to make jquery handle xhtml response + * + * fetches `url` and replaces this's content with the result + * + * @param mode how the replacement should be done (default is 'replace') + * Possible values are : + * - 'replace' to replace the node's content with the generated HTML + * - 'swap' to replace the node itself with the generated HTML + * - 'append' to append the generated HTML to the node's content + */ jQuery.fn.loadxhtml = function(url, data, reqtype, mode) { var ajax = null; if (reqtype == 'post') { @@ -323,7 +332,7 @@ } } -/* +/* XXX deprecates? * fetches `url` and replaces `nodeid`'s content with the result * @param replacemode how the replacement should be done (default is 'replace') * Possible values are : diff -r 0e3460341023 -r 7864fee8b4ec web/data/cubicweb.facets.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/cubicweb.facets.js Wed Aug 26 14:45:56 2009 +0200 @@ -0,0 +1,227 @@ +/* + * :organization: Logilab + * :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. + * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr + */ + +CubicWeb.require('htmlhelpers.js'); +CubicWeb.require('ajax.js'); + +//============= filter form functions ========================================// +function copyParam(origparams, newparams, param) { + var index = findValue(origparams[0], param); + if (index > -1) { + newparams[param] = origparams[1][index]; + } +} + +function facetFormContent(form) { + var names = []; + var values = []; + jQuery(form).find('.facet').each(function () { + var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName'); + var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) { + names.push(facetName); + values.push(this.getAttribute('cubicweb:value')); + }); + }); + jQuery(form).find('input').each(function () { + names.push(this.name); + values.push(this.value); + }); + jQuery(form).find('select option[selected]').each(function () { + names.push(this.parentNode.name); + values.push(this.value); + }); + return [names, values]; +} + +function buildRQL(divid, vid, paginate, vidargs) { + jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]); + var form = getNode(divid+'Form'); + var zipped = facetFormContent(form); + zipped[0].push('facetargs'); + zipped[1].push(vidargs); + var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]); + d.addCallback(function(result) { + var rql = result[0]; + var $bkLink = jQuery('#facetBkLink'); + if ($bkLink.length) { + var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql; + if (vid) { + bkUrl += '&vid=' + vid; + } + $bkLink.attr('href', bkUrl); + } + var toupdate = result[1]; + var extraparams = vidargs; + var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions'); + if (displayactions) { extraparams['displayactions'] = displayactions; } + if (paginate) { extraparams['paginate'] = '1'; } + // copy some parameters + // XXX cleanup vid/divid mess + // if vid argument is specified , the one specified in form params will + // be overriden by replacePageChunk + copyParam(zipped, extraparams, 'vid'); + extraparams['divid'] = divid; + copyParam(zipped, extraparams, 'divid'); + copyParam(zipped, extraparams, 'subvid'); + // paginate used to know if the filter box is acting, in which case we + // want to reload action box to match current selection (we don't want + // this from a table filter) + replacePageChunk(divid, rql, vid, extraparams, true, function() { + jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]); + }); + if (paginate) { + // FIXME the edit box might not be displayed in which case we don't + // know where to put the potential new one, just skip this case + // for now + if (jQuery('#edit_box').length) { + reloadComponent('edit_box', rql, 'boxes', 'edit_box'); + } + if (jQuery('#breadcrumbs').length) { + reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs'); + } + } + var d = asyncRemoteExec('filter_select_content', toupdate, rql); + d.addCallback(function(updateMap) { + for (facetId in updateMap) { + var values = updateMap[facetId]; + jqNode(facetId).find('.facetCheckBox').each(function () { + var value = this.getAttribute('cubicweb:value'); + if (!values.contains(value)) { + if (!jQuery(this).hasClass('facetValueDisabled')) { + jQuery(this).addClass('facetValueDisabled'); + } + } else { + if (jQuery(this).hasClass('facetValueDisabled')) { + jQuery(this).removeClass('facetValueDisabled'); + } + } + }); + } + }); + }); +} + + +var SELECTED_IMG = baseuri()+"data/black-check.png"; +var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png"; +var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png"; + +function initFacetBoxEvents(root) { + // facetargs : (divid, vid, paginate, extraargs) + root = root || document; + jQuery(root).find('form').each(function () { + var form = jQuery(this); + // NOTE: don't evaluate facetargs here but in callbacks since its value + // may changes and we must send its value when the callback is + // called, not when the page is initialized + var facetargs = form.attr('cubicweb:facetargs'); + if (facetargs !== undefined) { + form.submit(function() { + buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); + return false; + }); + form.find('div.facet').each(function() { + var facet = jQuery(this); + facet.find('div.facetCheckBox').each(function (i) { + this.setAttribute('cubicweb:idx', i); + }); + facet.find('div.facetCheckBox').click(function () { + var $this = jQuery(this); + if ($this.hasClass('facetValueDisabled')){ + return + } + if ($this.hasClass('facetValueSelected')) { + $this.removeClass('facetValueSelected'); + $this.find('img').each(function (i){ + if (this.getAttribute('cubicweb:unselimg')){ + this.setAttribute('src', UNSELECTED_BORDER_IMG); + this.setAttribute('alt', (_('not selected'))); + } + else{ + this.setAttribute('src', UNSELECTED_IMG); + this.setAttribute('alt', (_('not selected'))); + } + }); + var index = parseInt($this.attr('cubicweb:idx')); + // we dont need to move the element when cubicweb:idx == 0 + if (index > 0){ + var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) { + var nindex = parseInt(n.getAttribute('cubicweb:idx')); + return nindex > index; + }).length; + index += shift; + var parent = this.parentNode; + var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')'); + if ( ! ($insertAfter.length == 1 && shift == 0) ) { + // only rearrange element if necessary + $insertAfter.after(this); + } + } + } else { + var lastSelected = facet.find('.facetValueSelected:last'); + if (lastSelected.length) { + lastSelected.after(this); + } else { + var parent = this.parentNode; + jQuery(parent).prepend(this); + } + jQuery(this).addClass('facetValueSelected'); + var $img = jQuery(this).find('img'); + $img.attr('src', SELECTED_IMG).attr('alt', (_('selected'))); + } + buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); + facet.find('.facetBody').animate({scrollTop: 0}, ''); + }); + facet.find('select.facetOperator').change(function() { + var nbselected = facet.find('div.facetValueSelected').length; + if (nbselected >= 2) { + buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); + } + }); + facet.find('div.facetTitle').click(function() { + facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened'); + jQuery(this).toggleClass('opened'); + }); + + }); + } + }); +} + +// trigger this function on document ready event if you provide some kind of +// persistent search (eg crih) +function reorderFacetsItems(root){ + root = root || document; + jQuery(root).find('form').each(function () { + var form = jQuery(this); + if (form.attr('cubicweb:facetargs')) { + form.find('div.facet').each(function() { + var facet = jQuery(this); + var lastSelected = null; + facet.find('div.facetCheckBox').each(function (i) { + var $this = jQuery(this); + if ($this.hasClass('facetValueSelected')) { + if (lastSelected) { + lastSelected.after(this); + } else { + var parent = this.parentNode; + jQuery(parent).prepend(this); + } + lastSelected = $this; + } + }); + }); + } + }); +} + +// we need to differenciate cases where initFacetBoxEvents is called +// with one argument or without any argument. If we use `initFacetBoxEvents` +// as the direct callback on the jQuery.ready event, jQuery will pass some argument +// of his, so we use this small anonymous function instead. +jQuery(document).ready(function() {initFacetBoxEvents();}); + +CubicWeb.provide('facets.js'); diff -r 0e3460341023 -r 7864fee8b4ec web/data/cubicweb.form.css --- a/web/data/cubicweb.form.css Fri Aug 21 16:26:20 2009 +0200 +++ b/web/data/cubicweb.form.css Wed Aug 26 14:45:56 2009 +0200 @@ -78,7 +78,7 @@ table.attributeForm th, table.attributeForm td { - padding : .7em 2px; + padding : .2em 2px; } table.attributeForm th { diff -r 0e3460341023 -r 7864fee8b4ec web/data/cubicweb.formfilter.js --- a/web/data/cubicweb.formfilter.js Fri Aug 21 16:26:20 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,223 +0,0 @@ -/* - * :organization: Logilab - * :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. - * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr - */ - -CubicWeb.require('htmlhelpers.js'); -CubicWeb.require('ajax.js'); - -//============= filter form functions ========================================// -function copyParam(origparams, newparams, param) { - var index = findValue(origparams[0], param); - if (index > -1) { - newparams[param] = origparams[1][index]; - } -} - -function facetFormContent(form) { - var names = []; - var values = []; - jQuery(form).find('.facet').each(function () { - var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName'); - var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) { - names.push(facetName); - values.push(this.getAttribute('cubicweb:value')); - }); - }); - jQuery(form).find('input').each(function () { - names.push(this.name); - values.push(this.value); - }); - jQuery(form).find('select option[selected]').each(function () { - names.push(this.parentNode.name); - values.push(this.value); - }); - return [names, values]; -} - -function buildRQL(divid, vid, paginate, vidargs) { - jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]); - var form = getNode(divid+'Form'); - var zipped = facetFormContent(form); - zipped[0].push('facetargs'); - zipped[1].push(vidargs); - var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]); - d.addCallback(function(result) { - var rql = result[0]; - var $bkLink = jQuery('#facetBkLink'); - if ($bkLink.length) { - var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql; - if (vid) { - bkUrl += '&vid=' + vid; - } - $bkLink.attr('href', bkUrl); - } - var toupdate = result[1]; - var extraparams = vidargs; - var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions'); - if (displayactions) { extraparams['displayactions'] = displayactions; } - if (paginate) { extraparams['paginate'] = '1'; } - // copy some parameters - // XXX cleanup vid/divid mess - // if vid argument is specified , the one specified in form params will - // be overriden by replacePageChunk - copyParam(zipped, extraparams, 'vid'); - extraparams['divid'] = divid; - copyParam(zipped, extraparams, 'divid'); - copyParam(zipped, extraparams, 'subvid'); - // paginate used to know if the filter box is acting, in which case we - // want to reload action box to match current selection - replacePageChunk(divid, rql, vid, extraparams, true, function() { - jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]); - }); - if (paginate) { - // FIXME the edit box might not be displayed in which case we don't - // know where to put the potential new one, just skip this case - // for now - if (jQuery('#edit_box').length) { - reloadComponent('edit_box', rql, 'boxes', 'edit_box'); - } - } - var d = asyncRemoteExec('filter_select_content', toupdate, rql); - d.addCallback(function(updateMap) { - for (facetId in updateMap) { - var values = updateMap[facetId]; - jqNode(facetId).find('.facetCheckBox').each(function () { - var value = this.getAttribute('cubicweb:value'); - if (!values.contains(value)) { - if (!jQuery(this).hasClass('facetValueDisabled')) { - jQuery(this).addClass('facetValueDisabled'); - } - } else { - if (jQuery(this).hasClass('facetValueDisabled')) { - jQuery(this).removeClass('facetValueDisabled'); - } - } - }); - } - }); - }); -} - - -var SELECTED_IMG = baseuri()+"data/black-check.png"; -var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png"; -var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png"; - -function initFacetBoxEvents(root) { - // facetargs : (divid, vid, paginate, extraargs) - root = root || document; - jQuery(root).find('form').each(function () { - var form = jQuery(this); - // NOTE: don't evaluate facetargs here but in callbacks since its value - // may changes and we must send its value when the callback is - // called, not when the page is initialized - var facetargs = form.attr('cubicweb:facetargs'); - if (facetargs !== undefined) { - form.submit(function() { - buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); - return false; - }); - form.find('div.facet').each(function() { - var facet = jQuery(this); - facet.find('div.facetCheckBox').each(function (i) { - this.setAttribute('cubicweb:idx', i); - }); - facet.find('div.facetCheckBox').click(function () { - var $this = jQuery(this); - if ($this.hasClass('facetValueDisabled')){ - return - } - if ($this.hasClass('facetValueSelected')) { - $this.removeClass('facetValueSelected'); - $this.find('img').each(function (i){ - if (this.getAttribute('cubicweb:unselimg')){ - this.setAttribute('src', UNSELECTED_BORDER_IMG); - this.setAttribute('alt', (_('not selected'))); - } - else{ - this.setAttribute('src', UNSELECTED_IMG); - this.setAttribute('alt', (_('not selected'))); - } - }); - var index = parseInt($this.attr('cubicweb:idx')); - // we dont need to move the element when cubicweb:idx == 0 - if (index > 0){ - var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) { - var nindex = parseInt(n.getAttribute('cubicweb:idx')); - return nindex > index; - }).length; - index += shift; - var parent = this.parentNode; - var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')'); - if ( ! ($insertAfter.length == 1 && shift == 0) ) { - // only rearrange element if necessary - $insertAfter.after(this); - } - } - } else { - var lastSelected = facet.find('.facetValueSelected:last'); - if (lastSelected.length) { - lastSelected.after(this); - } else { - var parent = this.parentNode; - jQuery(parent).prepend(this); - } - jQuery(this).addClass('facetValueSelected'); - var $img = jQuery(this).find('img'); - $img.attr('src', SELECTED_IMG).attr('alt', (_('selected'))); - } - buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); - facet.find('.facetBody').animate({scrollTop: 0}, ''); - }); - facet.find('select.facetOperator').change(function() { - var nbselected = facet.find('div.facetValueSelected').length; - if (nbselected >= 2) { - buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs'))); - } - }); - facet.find('div.facetTitle').click(function() { - facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened'); - jQuery(this).toggleClass('opened'); - }); - - }); - } - }); -} - -// trigger this function on document ready event if you provide some kind of -// persistent search (eg crih) -function reorderFacetsItems(root){ - root = root || document; - jQuery(root).find('form').each(function () { - var form = jQuery(this); - if (form.attr('cubicweb:facetargs')) { - form.find('div.facet').each(function() { - var facet = jQuery(this); - var lastSelected = null; - facet.find('div.facetCheckBox').each(function (i) { - var $this = jQuery(this); - if ($this.hasClass('facetValueSelected')) { - if (lastSelected) { - lastSelected.after(this); - } else { - var parent = this.parentNode; - jQuery(parent).prepend(this); - } - lastSelected = $this; - } - }); - }); - } - }); -} - -// we need to differenciate cases where initFacetBoxEvents is called -// with one argument or without any argument. If we use `initFacetBoxEvents` -// as the direct callback on the jQuery.ready event, jQuery will pass some argument -// of his, so we use this small anonymous function instead. -jQuery(document).ready(function() {initFacetBoxEvents();}); - -CubicWeb.provide('formfilter.js'); diff -r 0e3460341023 -r 7864fee8b4ec web/facet.py --- a/web/facet.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/facet.py Wed Aug 26 14:45:56 2009 +0200 @@ -718,7 +718,7 @@ imgalt = self.req._('not selected') self.w(u'
\n' % (cssclass, xml_escape(unicode(self.value)))) - self.w(u'%s ' % (imgsrc, imgalt)) + self.w(u'%s ' % (imgsrc, imgalt)) self.w(u'%s' % xml_escape(self.label)) self.w(u'
') @@ -747,7 +747,7 @@ self.w(u'
\n' % (cssclass, xml_escape(unicode(self.value)))) self.w(u'
') - self.w(u'%s ' % (imgsrc, imgalt)) + self.w(u'%s ' % (imgsrc, imgalt)) self.w(u'' % (facetid, title)) self.w(u'
\n') self.w(u'
\n') @@ -755,7 +755,7 @@ class FacetSeparator(HTMLWidget): def __init__(self, label=None): - self.label = label or u' ' + self.label = label or u' ' def _render(self): pass diff -r 0e3460341023 -r 7864fee8b4ec web/form.py --- a/web/form.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/form.py Wed Aug 26 14:45:56 2009 +0200 @@ -162,7 +162,7 @@ if len(errors) > 1: templstr = '
  • %s
  • \n' else: - templstr = ' %s\n' + templstr = ' %s\n' for field, err in errors: if field is None: errormsg += templstr % err diff -r 0e3460341023 -r 7864fee8b4ec web/formwidgets.py --- a/web/formwidgets.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/formwidgets.py Wed Aug 26 14:45:56 2009 +0200 @@ -110,7 +110,7 @@ '
    ', tags.input(name=confirmname, value=values[0], type=self.type, **attrs), - ' ', tags.span(form.req._('confirm password'), + ' ', tags.span(form.req._('confirm password'), **{'class': 'emphasis'})] return u'\n'.join(inputs) @@ -437,7 +437,7 @@ return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
    -  
    +  
    ''' # buttons ###################################################################### diff -r 0e3460341023 -r 7864fee8b4ec web/htmlwidgets.py --- a/web/htmlwidgets.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/htmlwidgets.py Wed Aug 26 14:45:56 2009 +0200 @@ -75,7 +75,7 @@ self.w(u'\n') self.w(u'\n') if self.shadow: - self.w(u'
     
    ') + self.w(u'
     
    ') def _render(self): if self.id: @@ -191,7 +191,7 @@ self.value = value def _render(self): - self.w(u'
  • %s ' + self.w(u'
  • %s ' u'%s
  • ' % (self.label, self.value)) diff -r 0e3460341023 -r 7864fee8b4ec web/test/unittest_form.py --- a/web/test/unittest_form.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/test/unittest_form.py Wed Aug 26 14:45:56 2009 +0200 @@ -219,7 +219,7 @@ '''
    -  +  confirm password''' % {'eid': self.entity.eid}) diff -r 0e3460341023 -r 7864fee8b4ec web/views/basecomponents.py --- a/web/views/basecomponents.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/basecomponents.py Wed Aug 26 14:45:56 2009 +0200 @@ -74,7 +74,7 @@ id = 'help' cw_property_defs = VISIBLE_PROP_DEF def call(self): - self.w(u' ' + self.w(u' ' % (self.build_url(_restpath='doc/main'), self.req._(u'help'),)) @@ -110,11 +110,11 @@ def anon_user_link(self): if self.config['auth-mode'] == 'cookie': self.w(self.req._('anonymous')) - self.w(u''' [%s]''' + self.w(u''' [%s]''' % (self.req._('i18n_login_popup'))) else: self.w(self.req._('anonymous')) - self.w(u' [%s]' + self.w(u' [%s]' % (self.build_url('login'), self.req._('login'))) @@ -212,7 +212,7 @@ url, _('Any'))) else: html.insert(0, u'%s' % _('Any')) - self.w(u' | '.join(html)) + self.w(u' | '.join(html)) self.w(u'') class PdfViewComponent(component.Component): diff -r 0e3460341023 -r 7864fee8b4ec web/views/basetemplates.py --- a/web/views/basetemplates.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/basetemplates.py Wed Aug 26 14:45:56 2009 +0200 @@ -429,7 +429,7 @@ req._(ChangeLogView.title).lower())) self.w(u'%s | ' % (req.build_url('doc/about'), req._('about this site'))) - self.w(u'© 2001-2009 Logilab S.A.') + self.w(u'%s' % req._('powered by CubicWeb')) self.w(u'') @@ -478,7 +478,7 @@ self.w(u'
    ' % (id, klass)) if title: self.w(u'
    %s
    ' - % (self.req.property_value('ui.site-title') or u' ')) + % (self.req.property_value('ui.site-title') or u' ')) self.w(u'
    \n') if message: @@ -509,7 +509,7 @@ self.w(u'' % _('password')) self.w(u'\n') self.w(u'\n') - self.w(u' \n' % _('log in')) + self.w(u' \n' % _('log in')) self.w(u'\n') self.w(u'\n') self.w(u'\n') diff -r 0e3460341023 -r 7864fee8b4ec web/views/baseviews.py --- a/web/views/baseviews.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/baseviews.py Wed Aug 26 14:45:56 2009 +0200 @@ -51,13 +51,13 @@ """ id = 'final' # record generated i18n catalog messages - _('%d years') - _('%d months') - _('%d weeks') - _('%d days') - _('%d hours') - _('%d minutes') - _('%d seconds') + _('%d years') + _('%d months') + _('%d weeks') + _('%d days') + _('%d hours') + _('%d minutes') + _('%d seconds') _('%d years') _('%d months') _('%d weeks') @@ -80,7 +80,7 @@ # value is DateTimeDelta but we have no idea about what is the # reference date here, so we can only approximate years and months if format == 'text/html': - space = ' ' + space = ' ' else: space = ' ' if value.days > 730: # 2 years @@ -111,7 +111,7 @@ secondary = icon + view(oneline) """ entity = self.rset.get_entity(row, col) - self.w(u' ') + self.w(u' ') self.wview('oneline', self.rset, row=row, col=col) diff -r 0e3460341023 -r 7864fee8b4ec web/views/bookmark.py --- a/web/views/bookmark.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/bookmark.py Wed Aug 26 14:45:56 2009 +0200 @@ -33,7 +33,7 @@ def cell_call(self, row, col): """the primary view for bookmark entity""" entity = self.complete_entity(row, col) - self.w(u' ') + self.w(u' ') self.w(u"") self.w(u"%s : %s" % (self.req._('Bookmark'), xml_escape(entity.title))) self.w(u"") diff -r 0e3460341023 -r 7864fee8b4ec web/views/boxes.py --- a/web/views/boxes.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/boxes.py Wed Aug 26 14:45:56 2009 +0200 @@ -65,17 +65,8 @@ not self.schema[self.rset.description[0][0]].is_final() and \ searchstate == 'normal': entity = self.rset.get_entity(0, 0) - #entity.complete() - if add_menu.items: - self.info('explicit actions defined, ignoring potential rtags for %s', - entity.e_schema) - else: - # some addrelated actions may be specified but no one is selectable - # in which case we should not fallback to schema_actions. The proper - # way to avoid this is to override add_related_schemas() on the - # entity class to return an empty list - for action in self.schema_actions(entity): - add_menu.append(action) + for action in self.schema_actions(entity): + add_menu.append(action) self.workflow_actions(entity, box) if box.is_empty() and not other_menu.is_empty(): box.items = other_menu.items @@ -113,9 +104,9 @@ """this is actually used ui method to generate 'addrelated' actions from the schema. - If you're using explicit 'addrelated' actions for an entity types, you - should probably overrides this method to return an empty list else you - may get some unexpected actions. + 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 diff -r 0e3460341023 -r 7864fee8b4ec web/views/calendar.py --- a/web/views/calendar.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/calendar.py Wed Aug 26 14:45:56 2009 +0200 @@ -293,7 +293,7 @@ __redirectvid=self.id ) self.w(u'' % (xml_escape(url), self.req._(u'add'))) - self.w(u' ') + self.w(u' ') self.w(u'
    ') self.w(u'
    ') for task_descr in rows: @@ -312,7 +312,7 @@ self.w(u'
    ') else: self.w(u'
    ') - self.w(u" ") + self.w(u" ") self.w(u'
    ') self.w(u'
    ') self.w(u'') @@ -443,7 +443,7 @@ self.w(u'') self.w(u'') self.w(u'
    ') - self.w(u'
     
    ') + self.w(u'
     
    ') def _build_calendar_cell(self, date, task_descrs): inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()] diff -r 0e3460341023 -r 7864fee8b4ec web/views/cwuser.py --- a/web/views/cwuser.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/cwuser.py Wed Aug 26 14:45:56 2009 +0200 @@ -11,9 +11,10 @@ from cubicweb.selectors import one_line_rset, implements, match_user_groups from cubicweb.view import EntityView -from cubicweb.web import action +from cubicweb.web import action, uicfg from cubicweb.web.views import primary +uicfg.primaryview_section.tag_attribute(('CWUser', 'login'), 'hidden') class UserPreferencesEntityAction(action.Action): id = 'prefs' diff -r 0e3460341023 -r 7864fee8b4ec web/views/editcontroller.py --- a/web/views/editcontroller.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/editcontroller.py Wed Aug 26 14:45:56 2009 +0200 @@ -157,6 +157,7 @@ errorurl = self.req.form.get('__errorurl') if errorurl: self.req.cancel_edition(errorurl) + self.req.message = self.req._('edit canceled') return self.reset() def _action_delete(self): diff -r 0e3460341023 -r 7864fee8b4ec web/views/editviews.py --- a/web/views/editviews.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/editviews.py Wed Aug 26 14:45:56 2009 +0200 @@ -63,7 +63,7 @@ entity = self.rset.get_entity(row, col) erset = entity.as_rset() if self.req.match_search_state(erset): - self.w(u'%s [...]' % ( + self.w(u'%s [...]' % ( xml_escape(linksearch_select_url(self.req, erset)), self.req._('select this entity'), xml_escape(entity.view('textoutofcontext')), diff -r 0e3460341023 -r 7864fee8b4ec web/views/emailaddress.py --- a/web/views/emailaddress.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/emailaddress.py Wed Aug 26 14:45:56 2009 +0200 @@ -27,7 +27,7 @@ if not entity.canonical: canonemailaddr = entity.canonical_form() if canonemailaddr: - self.w(u' (%s)' % canonemailaddr.view('oneline')) + self.w(u' (%s)' % canonemailaddr.view('oneline')) self.w(u'') elif entity.identical_to: self.w(u'') diff -r 0e3460341023 -r 7864fee8b4ec web/views/facets.py --- a/web/views/facets.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/facets.py Wed Aug 26 14:45:56 2009 +0200 @@ -40,7 +40,7 @@ roundcorners = True needs_css = 'cubicweb.facets.css' - needs_js = ('cubicweb.ajax.js', 'cubicweb.formfilter.js') + needs_js = ('cubicweb.ajax.js', 'cubicweb.facets.js') bk_linkbox_template = u'
    %s
    ' diff -r 0e3460341023 -r 7864fee8b4ec web/views/formrenderers.py --- a/web/views/formrenderers.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/formrenderers.py Wed Aug 26 14:45:56 2009 +0200 @@ -109,7 +109,7 @@ if example: help.append('
    (%s: %s)
    ' % (self.req._('sample format'), example)) - return u' '.join(help) + return u' '.join(help) # specific methods (mostly to ease overriding) ############################# @@ -130,7 +130,7 @@ if len(errors) > 1: templstr = '
  • %s
  • \n' else: - templstr = ' %s\n' + templstr = ' %s\n' for field, err in errors: if field is None: errormsg += templstr % err @@ -284,7 +284,7 @@ if self.display_help: w(self.render_help(form, field)) # empty slot for buttons - w(u' ') + w(u' ') w(u'') w(u'') for field in fields: @@ -441,7 +441,7 @@ w(u'') pendings = list(form.restore_pending_inserts()) if not pendings: - w(u'  ') + w(u'  ') else: for row in pendings: # soon to be linked to entities @@ -519,7 +519,7 @@ w(u'+ %s.' % (rschema, entity.eid, js, __('add a %s' % targettype))) w(u'') - w(u'
     
    ') + w(u'
     
    ') w(u'') diff -r 0e3460341023 -r 7864fee8b4ec web/views/ibreadcrumbs.py --- a/web/views/ibreadcrumbs.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/ibreadcrumbs.py Wed Aug 26 14:45:56 2009 +0200 @@ -10,50 +10,55 @@ from logilab.mtconverter import xml_escape +from cubicweb.interfaces import IBreadCrumbs +from cubicweb.selectors import (one_line_rset, implements, one_etype_rset, + two_lines_rset, any_rset) +from cubicweb.view import EntityView, Component # don't use AnyEntity since this may cause bug with isinstance() due to reloading -from cubicweb.interfaces import IBreadCrumbs -from cubicweb.selectors import match_context_prop, one_line_rset, implements from cubicweb.entity import Entity -from cubicweb.view import EntityView from cubicweb.common.uilib import cut -from cubicweb.web.component import EntityVComponent def bc_title(entity): textsize = entity.req.property_value('navigation.short-line-size') return xml_escape(cut(entity.dc_title(), textsize)) +# XXX only provides the component version -class BreadCrumbEntityVComponent(EntityVComponent): +class BreadCrumbEntityVComponent(Component): id = 'breadcrumbs' - # register msg not generated since no entity implements IPrevNext in cubicweb itself + __select__ = one_line_rset() & implements(IBreadCrumbs) + + property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the component or not')), + } title = _('contentnavigation_breadcrumbs') help = _('contentnavigation_breadcrumbs_description') - __select__ = (one_line_rset() & match_context_prop() & implements(IBreadCrumbs)) - context = 'navtop' - order = 5 - visible = False - separator = u' > ' + separator = u' > ' def call(self, view=None, first_separator=True): entity = self.rset.get_entity(0,0) path = entity.breadcrumbs(view) if path: - self.w(u'') + self.w(u'') if first_separator: self.w(self.separator) - root = path.pop(0) - if isinstance(root, Entity): - self.w(u'%s' % (self.req.build_url(root.id), - root.dc_type('plural'))) - self.w(self.separator) - self.wpath_part(root, entity, not path) - for i, parent in enumerate(path): - self.w(self.separator) - self.w(u"\n") - self.wpath_part(parent, entity, i == len(path) - 1) + self.render_breadcrumbs(entity, path) self.w(u'') + def render_breadcrumbs(self, contextentity, path): + root = path.pop(0) + if isinstance(root, Entity): + self.w(u'%s' % (self.req.build_url(root.id), + root.dc_type('plural'))) + self.w(self.separator) + self.wpath_part(root, contextentity, not path) + for i, parent in enumerate(path): + self.w(self.separator) + self.w(u"\n") + self.wpath_part(parent, contextentity, i == len(path) - 1) + def wpath_part(self, part, contextentity, last=False): if isinstance(part, Entity): if last and part.eid == contextentity.eid: @@ -70,10 +75,28 @@ self.w(cut(unicode(part), textsize)) -class BreadCrumbComponent(BreadCrumbEntityVComponent): - __registry__ = 'components' - __select__ = (one_line_rset() & implements(IBreadCrumbs)) - visible = True +class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent): + __select__ = two_lines_rset() & one_etype_rset() & implements(IBreadCrumbs) + + def render_breadcrumbs(self, contextentity, path): + # XXX hack: only display etype name or first non entity path part + root = path.pop(0) + if isinstance(root, Entity): + self.w(u'%s' % (self.req.build_url(root.id), + root.dc_type('plural'))) + else: + self.wpath_part(root, entity, not path) + + +class BreadCrumbAnyRSetVComponent(BreadCrumbEntityVComponent): + __select__ = any_rset() + + def call(self, view=None, first_separator=True): + self.w(u'') + if first_separator: + self.w(self.separator) + self.w(self.req._('search')) + self.w(u'') class BreadCrumbView(EntityView): diff -r 0e3460341023 -r 7864fee8b4ec web/views/iprogress.py --- a/web/views/iprogress.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/iprogress.py Wed Aug 26 14:45:56 2009 +0200 @@ -167,10 +167,10 @@ """ id = 'ic_progress_table_view' - def call(self): + def call(self, columns=None): view = self.vreg['views'].select('progress_table_view', self.req, rset=self.rset) - columns = list(view.columns) + columns = list(columns or view.columns) try: columns.remove('project') except ValueError: diff -r 0e3460341023 -r 7864fee8b4ec web/views/management.py --- a/web/views/management.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/management.py Wed Aug 26 14:45:56 2009 +0200 @@ -139,7 +139,7 @@ # don't give __delete value to build_url else it will be urlquoted # and this will replace %s by %25s delurl += '&__delete=%s:require_permission:%%s' % entity.eid - dellinktempl = u'[-] ' % ( + dellinktempl = u'[-] ' % ( xml_escape(delurl), _('delete this permission')) else: dellinktempl = None diff -r 0e3460341023 -r 7864fee8b4ec web/views/navigation.py --- a/web/views/navigation.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/navigation.py Wed Aug 26 14:45:56 2009 +0200 @@ -40,9 +40,9 @@ self.index_display(start, stop))) start = stop + 1 w(u'') def index_display(self, start, stop): @@ -131,18 +131,18 @@ cell = self.format_link_content(index_display(start), index_display(stop)) blocklist.append(self.page_link(basepath, params, start, stop, cell)) start = stop + 1 - self.write_links(params, blocklist) + self.write_links(basepath, params, blocklist) def format_link_content(self, startstr, stopstr): text = u'%s - %s' % (startstr.lower()[:self.nb_chars], stopstr.lower()[:self.nb_chars]) return xml_escape(text) - def write_links(self, params, blocklist): + def write_links(self, basepath, params, blocklist): self.w(u'') @@ -160,7 +160,7 @@ # make a link to see them all if show_all_option: url = xml_escape(self.build_url(__force_display=1, **params)) - w(u'

    %s

    \n' + w(u'%s\n' % (url, req._('show %s results') % len(rset))) rset.limit(offset=start, limit=stop-start, inplace=True) diff -r 0e3460341023 -r 7864fee8b4ec web/views/old_calendar.py --- a/web/views/old_calendar.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/old_calendar.py Wed Aug 26 14:45:56 2009 +0200 @@ -30,8 +30,8 @@ # Navigation building methods / views #################################### - PREV = u'<<  <' - NEXT = u'>  >>' + PREV = u'<<  <' + NEXT = u'>  >>' NAV_HEADER = u"""
    %s%s
    @@ -200,7 +200,7 @@ self.w(u'') rql = self.rset.printable_rql() for cur_month in date_range(begin, end, incmonth=1): - umonth = u'%s %s' % (self.format_date(cur_month, '%B'), cur_month.year) + umonth = u'%s %s' % (self.format_date(cur_month, '%B'), cur_month.year) url = self.build_url(rql=rql, vid=self.id, year=cur_month.year, month=cur_month.month) self.w(u'%s' % (xml_escape(url), @@ -215,7 +215,7 @@ else: day = date(cur_month.year, cur_month.month, day_num+1) events = schedule.get(day) - self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1)) + self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1)) self.format_day_events(day, events) self.w(u'') @@ -345,8 +345,8 @@ am_row = [am for day, am, pm in row] pm_row = [pm for day, am, pm in row] formatted_rows.append('%s%s'% (week_title, '\n'.join(day_row))) - formatted_rows.append(' %s'% '\n'.join(am_row)) - formatted_rows.append(' %s'% '\n'.join(pm_row)) + formatted_rows.append(' %s'% '\n'.join(am_row)) + formatted_rows.append(' %s'% '\n'.join(pm_row)) # tigh everything together url = self.build_url(rql=rql, vid='ampmcalendarmonth', year=first_day.year, month=first_day.month) @@ -364,7 +364,7 @@ self.w(u'') rql = self.rset.printable_rql() for cur_month in date_range(begin, end, incmonth=1): - umonth = u'%s %s' % (self.format_date(cur_month, '%B'), cur_month.year) + umonth = u'%s %s' % (self.format_date(cur_month, '%B'), cur_month.year) url = self.build_url(rql=rql, vid=self.id, year=cur_month.year, month=cur_month.month) self.w(u'%s' % (xml_escape(url), @@ -379,7 +379,7 @@ else: day = date(cur_month.year, cur_month.month, day_num+1) events = schedule.get(day) - self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), + self.w(u'%s %s\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1)) self.format_day_events(day, events) self.w(u'') @@ -437,8 +437,8 @@ am_row = [am for day, am, pm in row] pm_row = [pm for day, am, pm in row] formatted_rows.append('%s%s'% (week_title, '\n'.join(day_row))) - formatted_rows.append(' %s'% '\n'.join(am_row)) - formatted_rows.append(' %s'% '\n'.join(pm_row)) + formatted_rows.append(' %s'% '\n'.join(am_row)) + formatted_rows.append(' %s'% '\n'.join(pm_row)) # tigh everything together url = self.build_url(rql=rql, vid='ampmcalendarmonth', year=first_day.year, month=first_day.month) @@ -464,7 +464,7 @@ monthlink = '%s' % (xml_escape(url), umonth) w(u'%s' % ( WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink))) - w(u'%s '% _(u'Date')) + w(u'%s '% _(u'Date')) for day in date_range(monday, sunday): events = schedule.get(day) style = day.weekday() % 2 and "even" or "odd" @@ -534,9 +534,9 @@ AMPM_CONTENT = u'%s
    %s
    ' WEEK_TITLE = u'%s %s (%s)' -WEEK_EMPTY_CELL = u' ' +WEEK_EMPTY_CELL = u' ' WEEK_CELL = u'
    %s
    ' -AMPM_DAYWEEK_EMPTY = u'%s %s' -AMPM_DAYWEEK = u'%s %s' +AMPM_DAYWEEK_EMPTY = u'%s %s' +AMPM_DAYWEEK = u'%s %s' AMPM_WEEK_CELL = u'
    %02d:%02d - %s
    ' diff -r 0e3460341023 -r 7864fee8b4ec web/views/startup.py --- a/web/views/startup.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/startup.py Wed Aug 26 14:45:56 2009 +0200 @@ -52,10 +52,10 @@ self.wview('inlined', rset, row=0) else: self.entities() - self.w(u'
     
    ') + self.w(u'
     
    ') self.startup_views() if manager and 'Card' in self.schema: - self.w(u'
     
    ') + self.w(u'
     
    ') if rset: href = rset.get_entity(0, 0).absolute_url(vid='edition') label = self.req._('edit the index page') @@ -104,7 +104,7 @@ key=lambda (l,a,e):unormalize(l)) q, r = divmod(len(infos), 2) if r: - infos.append( (None, ' ', ' ') ) + infos.append( (None, ' ', ' ') ) infos = zip(infos[:q+r], infos[q+r:]) for (_, etypelink, addlink), (_, etypelink2, addlink2) in infos: self.w(u'\n') @@ -126,7 +126,7 @@ label = display_name(req, etype, 'plural') nb = req.execute('Any COUNT(X) WHERE X is %s' % etype)[0][0] url = self.build_url(etype) - etypelink = u' %s (%d)' % ( + etypelink = u' %s (%d)' % ( xml_escape(url), label, nb) yield (label, etypelink, self.add_entity_link(eschema, req)) diff -r 0e3460341023 -r 7864fee8b4ec web/views/tableview.py --- a/web/views/tableview.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/tableview.py Wed Aug 26 14:45:56 2009 +0200 @@ -51,7 +51,7 @@ """display a form to filter table's content. This should only occurs when a context eid is given """ - self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.formfilter.js')) + self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.facets.js')) # drop False / None values from vidargs vidargs = dict((k, v) for k, v in vidargs.iteritems() if v) self.w(u'
    ' % @@ -129,8 +129,7 @@ # replace the inner div, so don't regenerate everything under the if # below if not fromformfilter: - div_class = 'section' - self.w(u'
    ' % div_class) + self.w(u'
    ') if not title and 'title' in req.form: title = req.form['title'] if title: diff -r 0e3460341023 -r 7864fee8b4ec web/views/timetable.py --- a/web/views/timetable.py Fri Aug 21 16:26:20 2009 +0200 +++ b/web/views/timetable.py Wed Aug 26 14:45:56 2009 +0200 @@ -133,7 +133,7 @@ """ render column headers """ self.w(u'\n') - self.w(u' \n') + self.w(u' \n') columns = [] for user, width in zip(users, widths): self.w(u'' % max(MIN_COLS, width)) @@ -191,13 +191,13 @@ task_descr, first_row = value if first_row: url = xml_escape(task_descr.task.absolute_url(vid="edition")) - self.w(u' 
    ' % ( + self.w(u' 
    ' % ( task_descr.lines, task_descr.color, filled_klasses[kj], url)) task_descr.task.view('tooltip', w=self.w) self.w(u'
    ') else: if empty_line: - self.w(u' ') + self.w(u' ') else: - self.w(u' ' % empty_klasses[kj] ) + self.w(u' ' % empty_klasses[kj] ) self.w(u'\n') diff -r 0e3460341023 -r 7864fee8b4ec web/wdoc/ChangeLog_en --- a/web/wdoc/ChangeLog_en Fri Aug 21 16:26:20 2009 +0200 +++ b/web/wdoc/ChangeLog_en Wed Aug 26 14:45:56 2009 +0200 @@ -1,27 +1,34 @@ .. -*- coding: utf-8 -*- -.. _`user preferences`: myprefs#fieldset_ui - -2008-09-25 -- 2.50.0 - * jQuery replaces MochiKit - * schema inheritance support +.. _`user preferences`: myprefs +.. _here: sparql +.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/ +.. _schema: schema +.. _OWL: http://www.w3.org/TR/owl-features/ -2008-05-13 -- 2.48.0 - * web pages are now served with the ``xhtml+xml`` content type +2009-08-07 -- 3.4.0 + + * support for SPARQL_. Click here_ to test it. -2008-03-27 -- 2.47.0 - * fckeditor is now integrated to edit rich text fields. If you don't see it, - check your `user preferences`_. + * and another step toward the semantic web: new `ExternalUri` entity type + with its associated `same_as` relation. See + http://www.w3.org/TR/owl-ref/#sameAs-def for more information and check + this instance schema_ to see on which entity types it may be applied -2008-03-13 -- 2.46.0 - * new calendar and timetable views. - - * click-and-edit functionalities : if you see the text edit cursor when - you're over a fied, try to double-click! - - * automatic facets oriented search : a filter box should appear when you're - looking for something and more than one entity are displayed. + * new "view workflow" and "view history" items in the workflow + sub-menu of the actions box + + * you can now edit comments of workflow transition afterward (if authorized, + of course) + + * modification date of an entity is updated when its state is changed + -2008-02-15 -- 2.44.0 - * new internationalized online help system. Click the question mark on the - right top corner! Hopefuly some new documentation will appear as time is - going. +2009-02-26 -- 3.1.0 + + * schema may be exported as OWL_ + + * new ajax interface for site configuration / `user preferences`_ + + +2008-10-24 -- 2.99.0 + * cubicweb is now open source ! diff -r 0e3460341023 -r 7864fee8b4ec web/wdoc/ChangeLog_fr --- a/web/wdoc/ChangeLog_fr Fri Aug 21 16:26:20 2009 +0200 +++ b/web/wdoc/ChangeLog_fr Wed Aug 26 14:45:56 2009 +0200 @@ -1,31 +1,34 @@ .. -*- coding: utf-8 -*- .. _`préférences utilisateurs`: myprefs#fieldset_ui - -2008-09-25 -- 2.50.0 - * jQuery remplace MochiKit - * support de l'héritage de schéma +.. _ici: sparql +.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/ +.. _schema: schema +.. _OWL: http://www.w3.org/TR/owl-features/ -2008-05-13 -- 2.48.0 - * les pages sont servies en tant que ``xhtml+xml`` pour certains navigateurs +2009-08-07 -- 3.4.0 -2008-03-27 -- 2.47.0 - * fckeditor est enfin intégré pour éditer les champs de type texte riche. Si - vous ne le voyez pas apparaître, vérifiez vos `préférences utilisateurs`_. + * support de SPARQL_. Cliquez ici_ pour le tester. -2008-03-13 -- 2.46.0 - * nouvelle vues calendrier et emploi du temps - - * fonctionalité "click-et-édite" : si vous voyez apparaitre le curseur - d'édition de texte en survolant un champ, essayez de double-cliquer ! - - * recherche par facettes : une boîte de filtrage devrait apparaitre - automatiquement lorsque vous effectuez une recherche qui ramène plus d'une - entité + * et encore un pas vers le web sémantique : un nouveau type d'entité + `ExternalUri` et la relation associée `same_as`. Voir + http://www.w3.org/TR/owl-ref/#sameAs-def pour plus d'information, ainsi + que le schema_ de cette instance pour voir à quels types d'entités cela + s'applique. -2008-02-15 -- 2.44.0 - * nouveau système d'aide internationalisé. Cliquez sur le point - d'interrogation en haut à droite. Reste à enrichir le contenu de cette - documentation, mais cela devrait arriver avec le temps. + * nouveau liens "voir les états possibles" et "voir l'historique" dans le sous-menu + workflow de la boite actions + + * vous pouvez dorénavant éditer les commentaires des passages de transition + depuis l'historique, pour peu que vous ayez les droits nécessaire bien sûr + + * la date de modification d'une entité est mise à jour lorsque son état est changé - +2009-02-26 -- 3.1.0 + + * le schéma peut être exporté en OWL_ + + * nouvelle interface ajax pour la configuration du site et les `préférences utilisateurs`_ + + +