class RelationBox(component.CtxComponent): """ Helper view class to display a relation rset in a sidebox. """ __select__ = nonempty_rset() & match_kwargs("title", "rql") __regid__ = "relationbox" cw_property_defs = {} context = "incontext" @property def domid(self): return (super(RelationBox, self).domid + unicode(abs(id(self))) + unicode(abs(id(self.cw_rset)))) def render_title(self, w): w(self.cw_extra_kwargs["title"]) def render_body(self, w): defaultlimit = self._cw.property_value("navigation.related-limit") if not isinstance(self.cw_rset, list): rset = list(self.cw_rset.entities()) else: rset = self.cw_rset for entity in rset[:(defaultlimit - 1)]: w(u"<div>• " + entity.view(self.context) + u"</div>") # if len(rset) == defaultlimit: rql = self.cw_extra_kwargs["rql"] href = self._cw.build_url(rql=rql) w(u"<br/><div><a href='{0}'>↺ see more</a></div>".format(href))
class InlineAddNewLinkView(InlineEntityCreationFormView): """ :attr card: the cardinality of the relation according to role of `peid` """ __regid__ = 'inline-addnew-link' __select__ = (match_kwargs('peid', 'petype', 'rtype') & specified_etype_implements('Any')) _select_attrs = InlineEntityCreationFormView._select_attrs + ('card', ) card = None # make pylint happy form = None # no actual form wrapped def call(self, i18nctx, **kwargs): self._cw.set_varmaker() divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid) self.w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">' % divid) js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s', '%s')" % ( self.peid, self.petype, self.etype, self.rtype, self.role, i18nctx) if self.pform.should_hide_add_new_relation_link(self.rtype, self.card): js = "toggleVisibility('%s'); %s" % (divid, js) __ = self._cw.pgettext self.w( u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>' % (self.rtype, self.peid, js, __(i18nctx, 'add a %s' % self.etype))) self.w(u'</div>')
class GroupByView(EntityView): """grouped view of a result set. The `group_key` method return the group key of an entities (a string or tuple of string). For each group, display a link to entities of this group by generating url like <basepath>/<key> or <basepath>/<key item 1>/<key item 2>. """ __abstract__ = True __select__ = EntityView.__select__ & match_kwargs('basepath') entity_attribute = None reversed = False def index_url(self, basepath, key, **kwargs): if isinstance(key, (list, tuple)): key = '/'.join(key) return self._cw.build_url('%s/%s' % (basepath, key), **kwargs) def index_link(self, basepath, key, items): url = self.index_url(basepath, key) if isinstance(key, (list, tuple)): key = ' '.join(key) return tags.a(key, href=url) def group_key(self, entity, **kwargs): value = getattr(entity, self.entity_attribute) if callable(value): value = value() return value def call(self, basepath, maxentries=None, **kwargs): index = {} for entity in self.cw_rset.entities(): index.setdefault(self.group_key(entity, **kwargs), []).append(entity) displayed = sorted(index) if self.reversed: displayed = reversed(displayed) if maxentries is None: needmore = False else: needmore = len(index) > maxentries displayed = tuple(displayed)[:maxentries] w = self.w w(u'<ul class="boxListing">') for key in displayed: if key: w(u'<li>%s</li>\n' % self.index_link(basepath, key, index[key])) if needmore: url = self._cw.build_url('view', vid=self.__regid__, rql=self.cw_rset.printable_rql()) w(u'<li>%s</li>\n' % tags.a(u'[%s]' % self._cw._('see more'), href=url)) w(u'</ul>\n')
class UndoableTransactionView(View): __regid__ = 'undoable-transaction-view' __select__ = View.__select__ & match_kwargs('txuuid') item_vid = 'undoable-action-list-view' cache_max_age = 0 def build_undo_link(self, txuuid, redirect_path=None, redirect_params=None): """ the kwargs are passed to build_url""" _ = self._cw._ redirect = {} if redirect_path: redirect['__redirectpath'] = redirect_path if redirect_params: if isinstance(redirect_params, dict): redirect['__redirectparams'] = self._cw.build_url_params( **redirect_params) else: redirect['__redirectparams'] = redirect_params link_url = self._cw.build_url('undo', txuuid=txuuid, **redirect) msg = u"<span class='undo'>%s</span>" % tags.a(_('undo'), href=link_url) return msg def call(self, txuuid, public=True, redirect_path=None, redirect_params=None): _ = self._cw._ txinfo = self._cw.cnx.transaction_info(txuuid) try: #XXX Under some unknown circumstances txinfo.user_eid=-1 user = self._cw.entity_from_eid(txinfo.user_eid) except UnknownEid: user = None undo_url = self.build_undo_link(txuuid, redirect_path=redirect_path, redirect_params=redirect_params) txinfo_dict = dict(dt=self._cw.format_date(txinfo.datetime, time=True), user_eid=txinfo.user_eid, user=user and user.view('outofcontext') or _("undefined user"), txuuid=txuuid, undo_link=undo_url) self.w(_("By %(user)s on %(dt)s [%(undo_link)s]") % txinfo_dict) tx_actions = txinfo.actions_list(public=public) if tx_actions: self.wview(self.item_vid, None, tx_actions=tx_actions)
class PyValListView(View): """display a list of values into an html list. Take care, content is NOT xml-escaped. """ __regid__ = 'pyvallist' __select__ = match_kwargs('pyvalue') def call(self, pyvalue): self.w(u'<ul>\n') for line in pyvalue: self.w(u'<li>%s</li>\n' % line) self.w(u'</ul>\n')
class URLAttributeView(EntityView): """:__regid__: *urlattr* This view will wrap an attribute value (hence expect a string) into an '<a>' HTML tag to display a clickable link. """ __regid__ = 'urlattr' __select__ = EntityView.__select__ & match_kwargs('rtype') def entity_call(self, entity, rtype, **kwargs): url = entity.printable_value(rtype) if url: self.w(u'<a href="%s">%s</a>' % (url, url))
class AddRelationView(component.EditRelationMixIn, View): """base class for view which let add entities linked by a given relation subclasses should define at least id, rtype and target class attributes. """ __registry__ = 'views' __regid__ = 'xaddrelation' __select__ = (match_form_params('rtype', 'target') | match_kwargs('rtype', 'target')) cw_property_defs = {} # don't want to inherit this from Box expected_kwargs = form_params = ('rtype', 'target') def cell_call(self, row, col, rtype=None, target=None, etype=None): self.rtype = rtype or self._cw.form['rtype'] self.target = target or self._cw.form['target'] self.etype = etype or self._cw.form.get('etype') entity = self.cw_rset.get_entity(row, col) rschema = self._cw.vreg.schema.rschema(self.rtype) if not self.etype: if self.target == 'object': etypes = rschema.objects(entity.e_schema) else: etypes = rschema.subjects(entity.e_schema) if len(etypes) == 1: self.etype = etypes[0] self.w(u'<div id="%s">' % self.domid) self.w( u'<h1>%s</h1>' % self._cw._('relation %(relname)s of %(ent)s') % { 'relname': rschema.display_name(self._cw, role(self)), 'ent': entity.view('incontext') }) self.w(u'<ul class="list-unstyled">') for boxitem in self.unrelated_boxitems(entity): self.w('<li>%s</li>' % boxitem) self.w(u'</ul></div>') def unrelated_entities(self, entity): """returns the list of unrelated entities if etype is not defined on the Box's class, the default behaviour is to use the entity's appropraite vocabulary function """ # use entity.unrelated if we've been asked for a particular etype if getattr(self, 'etype', None): rset = entity.unrelated(self.rtype, self.etype, role(self), ordermethod='fetch_order') self.paginate(self._cw, rset=rset, w=self.w) return rset.entities() super(AddRelationView, self).unrelated_entities(self)
class VerbatimAttributeView(EntityView): """:__regid__: *verbatimattr* This view will wrap an attribute value into an '<pre>' HTML tag to display arbitrary text where EOL will be respected. It usually make sense for attributes whose value is a multi-lines string where new lines matters. """ __regid__ = 'verbatimattr' __select__ = EntityView.__select__ & match_kwargs('rtype') def entity_call(self, entity, rtype, **kwargs): value = entity.printable_value(rtype) if value: self.w(u'<pre>%s</pre>' % value)
class DataFeedSourceDataImport(EntityView): __select__ = EntityView.__select__ & match_kwargs('rtype') __regid__ = 'cw.formated_log' def cell_call(self, row, col, rtype, loglevel='Info', **kwargs): if 'dispctrl' in self.cw_extra_kwargs: loglevel = self.cw_extra_kwargs['dispctrl'].get( 'loglevel', loglevel) entity = self.cw_rset.get_entity(row, col) value = getattr(entity, rtype) if value: self._cw.view('cw.log.table', pyvalue=log_to_table(self._cw, value), default_level=loglevel, w=self.w) else: self.w(self._cw._('no log to display'))
class UndoableActionListView(View): __regid__ = 'undoable-action-list-view' __select__ = View.__select__ & match_kwargs('tx_actions') title = _('Undoable actions') item_vid = 'undoable-action-view' cache_max_age = 0 def call(self, tx_actions): if tx_actions : self.w(u"<ol class='undo-actions'>") for action in tx_actions: self.cell_call(action) self.w(u"</ol>") def cell_call(self, action): self.w(u'<li>') self.wview(self.item_vid, None, tx_action=action) self.w(u'</li>\n')
class AttributeView(EntityView): """:__regid__: *attribute* This view is generally used to disable the *reledit* feature. It works on both relations and attributes. """ __regid__ = 'attribute' __select__ = EntityView.__select__ & match_kwargs('rtype') def entity_call(self, entity, rtype, role='subject', **kwargs): if self._cw.vreg.schema.rschema(rtype).final: self.w(entity.printable_value(rtype)) else: dispctrl = uicfg.primaryview_display_ctrl.etype_get( entity.e_schema, rtype, role, '*') rset = entity.related(rtype, role) if rset: self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
class InlineEntityCreationFormView(InlineEntityEditionFormView): """ :attr etype: the entity type being created in the inline form """ __regid__ = 'inline-creation' __select__ = (match_kwargs('peid', 'petype', 'rtype') & specified_etype_implements('Any')) _select_attrs = InlineEntityEditionFormView._select_attrs + ('petype', ) # make pylint happy petype = None @property def removejs(self): entity = self._entity() rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype) card = rdef.role_cardinality(self.role) # when one is adding an inline entity for a relation of a single card, # the 'add a new xxx' link disappears. If the user then cancel the addition, # we have to make this link appears back. This is done by giving add new link # id to removeInlineForm. if card == '?': divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid) return "removeInlineForm('%%s', '%%s', '%s', '%%s', '%s')" % ( self.role, divid) elif card in '+*': return "removeInlineForm('%%s', '%%s', '%s', '%%s')" % self.role # don't do anything for card == '1' @cached def _entity(self): try: cls = self._cw.vreg['etypes'].etype_class(self.etype) except Exception: self.w(self._cw._('no such entity type %s') % self.etype) return entity = cls(self._cw) entity.eid = next(self._cw.varmaker) return entity def call(self, i18nctx, **kwargs): self.render_form(i18nctx, **kwargs)
class OneLineSelectableView(EntityView): """custom oneline view used by company / division treeview""" __regid__ = 'oneline-selectable' __select__ = is_instance('Company') & match_kwargs('onscreen') def cell_call(self, row, col, onscreen): entity = self.cw_rset.get_entity(row, col) if entity.eid == onscreen: self.w(u'<span class="currentCompany">%s</span>' % xml_escape(entity.view('textincontext'))) else: if entity.headquarters: tooltip = xml_escape(entity.headquarters[0].dc_long_title()) else: tooltip = u'' self.w(u'<a href="%s" title="%s">%s</a>' % (xml_escape(entity.absolute_url()), xml_escape(tooltip), xml_escape(entity.dc_title())))
class RegisterUserService(Service): """check if a user with the given login exists, if not create it with the given password. This service is designed to be used for anonymous registration on public web sites. To use it, do: with self.appli.repo.internal_cnx() as cnx: cnx.call_service('register_user', login=login, password=password, **cwuserkwargs) """ __regid__ = 'register_user' __select__ = Service.__select__ & match_kwargs('login', 'password') default_groups = ('users', ) def call(self, login, password, email=None, groups=None, **cwuserkwargs): cnx = self._cw if isinstance(password, str): # password should *always* be utf8 encoded password = password.encode('UTF8') cwuserkwargs['login'] = login cwuserkwargs['upassword'] = password # we have to create the user user = cnx.create_entity('CWUser', **cwuserkwargs) if groups is None: groups = self.default_groups assert groups, "CWUsers must belong to at least one CWGroup" group_names = ', '.join('%r' % group for group in groups) cnx.execute( 'SET X in_group G WHERE X eid %%(x)s, G name IN (%s)' % group_names, {'x': user.eid}) if email or '@' in login: d = {'login': login, 'email': email or login} cnx.execute( 'INSERT EmailAddress X: X address %(email)s, ' 'U primary_email X, U use_email X ' 'WHERE U login %(login)s', d, build_descr=False) return user
class RsetBox(component.CtxComponent): """helper view class to display an rset in a sidebox""" __select__ = nonempty_rset() & match_kwargs('title', 'vid') __regid__ = 'rsetbox' cw_property_defs = {} context = 'incontext' @property def domid(self): return super(RsetBox, self).domid + str(abs(id(self))) + str(abs(id(self.cw_rset))) def render_title(self, w): w(self.cw_extra_kwargs['title']) def render_body(self, w): if 'dispctrl' in self.cw_extra_kwargs: # XXX do not modify dispctrl! self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext') self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1) self._cw.view(self.cw_extra_kwargs['vid'], self.cw_rset, w=w, initargs=self.cw_extra_kwargs)
class LogFormView(View): # XXX an awful lot of hardcoded assumptions there # makes it unobvious to reuse/specialize __regid__ = 'logform' __select__ = match_kwargs('id', 'klass') title = 'log in' def call(self, id, klass, title=True, showmessage=True): w = self.w w(u'<div id="%s" class="%s">' % (id, klass)) if title: stitle = self._cw.property_value('ui.site-title') if stitle: stitle = xml_escape(stitle) else: stitle = u' ' w(u'<div class="loginTitle">%s</div>' % stitle) w(u'<div class="loginContent">\n') # don't call self._cw.message twice since it pops the id message = self._cw.message if showmessage and message: w(u'<div class="loginMessage">%s</div>\n' % message) config = self._cw.vreg.config if config['auth-mode'] != 'http': self.login_form(id) # Cookie authentication w(u'</div>') w(u'</div>\n') def login_form(self, id): cw = self._cw form = cw.vreg['forms'].select('logform', cw) if cw.vreg.config['allow-email-login']: label = cw._('login or email') else: label = cw.pgettext('CWUser', 'login') form.field_by_name('__login').label = label form.render(w=self.w, table_class='', display_progress_div=False) cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
def test_match_kwargs_any(self): selector = match_kwargs( set( ('a', 'b') ), mode='any') self.assertEqual(selector(None, None, a=1, b=2), 2) self.assertEqual(selector(None, None, a=1), 1) self.assertEqual(selector(None, None, c=1), 0) self.assertEqual(selector(None, None, a=1, c=1), 1)
def test_match_kwargs_default(self): selector = match_kwargs( set( ('a', 'b') ) ) self.assertEqual(selector(None, None, a=1, b=2), 2) self.assertEqual(selector(None, None, a=1), 0) self.assertEqual(selector(None, None, c=1), 0) self.assertEqual(selector(None, None, a=1, c=1), 0)
class RsetTableView(TableMixIn, AnyRsetView): """This table view accepts any non-empty rset. It uses introspection on the result set to compute column names and the proper way to display the cells. It is highly configurable and accepts a wealth of options, but take care to check what you're trying to achieve wouldn't be a job for the :class:`EntityTableView`. Basically the question is: does this view should be tied to the result set query's shape or no? If yes, than you're fine. If no, you should take a look at the other table implementation. The following class attributes may be used to control the table: * `finalvid`, a view identifier that should be called on final entities (e.g. attribute values). Default to 'final'. * `nonfinalvid`, a view identifier that should be called on entities. Default to 'incontext'. * `displaycols`, if not `None`, should be a list of rset's columns to be displayed. * `headers`, if not `None`, should be a list of headers for the table's columns. `None` values in the list will be replaced by computed column names. * `cellvids`, if not `None`, should be a dictionary with table column index as key and a view identifier as value, telling the view that should be used in the given column. Notice `displaycols`, `headers` and `cellvids` may be specified at selection time but then the table won't have pagination and shouldn't be configured to display the facets filter nor actions (as they wouldn't behave as expected). This table class use the :class:`RsetTableColRenderer` as default column renderer. .. autoclass:: RsetTableColRenderer """ #'# make emacs happier __regid__ = 'table' # selector trick for bw compath with the former :class:TableView __select__ = AnyRsetView.__select__ & (~match_kwargs('title', 'subvid', 'displayfilter', 'headers', 'displaycols', 'displayactions', 'actions', 'divid', 'cellvids', 'cellattrs', 'mainindex', 'paginate', 'page_size', mode='any') | unreloadable_table()) title = _('table') # additional configuration parameters finalvid = 'final' nonfinalvid = 'incontext' displaycols = None headers = None cellvids = None default_column_renderer_class = RsetTableColRenderer def linkable(self): # specific subclasses of this view usually don't want to be linkable # since they depends on a particular shape (being linkable meaning view # may be listed in possible views return self.__regid__ == 'table' def call(self, headers=None, displaycols=None, cellvids=None, paginate=None): if self.headers: self.headers = [h and self._cw._(h) for h in self.headers] if (headers or displaycols or cellvids or paginate): if headers is not None: self.headers = headers if displaycols is not None: self.displaycols = displaycols if cellvids is not None: self.cellvids = cellvids if paginate is not None: self.paginable = paginate super(RsetTableView, self).call() def main_var_index(self): """returns the index of the first non-attribute variable among the RQL selected variables """ eschema = self._cw.vreg.schema.eschema for i, etype in enumerate(self.cw_rset.description[0]): if not eschema(etype).final: return i return None # layout callbacks ######################################################### @property def table_size(self): """return the number of rows (header excluded) to be displayed""" return self.cw_rset.rowcount def build_column_renderers(self): headers = self.headers # compute displayed columns if self.displaycols is None: if headers is not None: displaycols = list(range(len(headers))) else: rqlst = self.cw_rset.syntax_tree() displaycols = list(range(len(rqlst.children[0].selection))) else: displaycols = self.displaycols # compute table headers main_var_index = self.main_var_index() computed_titles = self.columns_labels(main_var_index) # compute build renderers cellvids = self.cellvids renderers = [] for colnum, colid in enumerate(displaycols): addcount = False # compute column header title = None if headers is not None: title = headers[colnum] if title is None: title = computed_titles[colid] if colid == main_var_index: addcount = True # compute cell vid for the column if cellvids is not None and colnum in cellvids: cellvid = cellvids[colnum] else: coltype = self.cw_rset.description[0][colid] if coltype is not None and self._cw.vreg.schema.eschema( coltype).final: cellvid = self.finalvid else: cellvid = self.nonfinalvid # get renderer renderer = self.column_renderer(colid, header=title, trheader=False, addcount=addcount, cellvid=cellvid) renderers.append(renderer) return renderers
class PyValTableView(tableview.TableMixIn, View): """This table view is designed to be used a list of list of unicode values given as a mandatory `pyvalue` argument. Take care, content is NOT xml-escaped. It's configured through the following selection arguments. If `headers` is specified, it is expected to be a list of headers to be inserted as first row (in <thead>). `header_column_idx` may be used to specify a column index or a set of column indiced where values should be inserted inside <th> tag instead of <td>. `cssclass` is the CSS class used on the <table> tag, and default to 'listing' (so that the table will look similar to those generated by the table view). """ __regid__ = 'pyvaltable' __select__ = match_kwargs('pyvalue') default_column_renderer_class = PyValTableColRenderer paginable = False # not supported headers = None cssclass = None domid = None def __init__(self, req, pyvalue, headers=None, cssclass=None, header_column_idx=None, **kwargs): super(PyValTableView, self).__init__(req, **kwargs) self.pyvalue = pyvalue if headers is not None: self.headers = headers elif self.headers: # headers set on a class attribute, translate self.headers = [self._cw._(header) for header in self.headers] if cssclass is not None: self.cssclass = cssclass self.header_column_idx = header_column_idx @property def layout_args(self): args = {} if self.cssclass: args['cssclass'] = self.cssclass if self.header_column_idx is not None: args['header_column_idx'] = self.header_column_idx return args # layout callbacks ######################################################### @property def table_size(self): """return the number of rows (header excluded) to be displayed""" return len(self.pyvalue) @property def has_headers(self): return self.headers def build_column_renderers(self): return [ self.column_renderer(colid) for colid in range(len(self.pyvalue[0])) ] def facets_form(self, mainvar=None): return None # not supported def table_actions(self): return [] # not supported
class InlineEntityEditionFormView(f.FormViewMixIn, EntityView): """ :attr peid: the parent entity's eid hosting the inline form :attr rtype: the relation bridging `etype` and `peid` :attr role: the role played by the `peid` in the relation :attr pform: the parent form where this inlined form is being displayed """ __regid__ = 'inline-edition' __select__ = non_final_entity() & match_kwargs('peid', 'rtype') _select_attrs = ('peid', 'rtype', 'role', 'pform', 'etype') removejs = "removeInlinedEntity('%s', '%s', '%s')" form_renderer_id = 'inline' # make pylint happy peid = rtype = role = pform = etype = None def __init__(self, *args, **kwargs): for attr in self._select_attrs: # don't pop attributes from kwargs, so the end-up in # self.cw_extra_kwargs which is then passed to the edition form (see # the .form method) setattr(self, attr, kwargs.get(attr)) super(InlineEntityEditionFormView, self).__init__(*args, **kwargs) def _entity(self): assert self.cw_row is not None, self return self.cw_rset.get_entity(self.cw_row, self.cw_col) @property def petype(self): assert isinstance(self.peid, int) pentity = self._cw.entity_from_eid(self.peid) return pentity.e_schema.type @property @cached def form(self): entity = self._entity() form = self._cw.vreg['forms'].select( 'edition', self._cw, entity=entity, formtype='inlined', form_renderer_id=self.form_renderer_id, copy_nav_params=False, mainform=False, parent_form=self.pform, **self.cw_extra_kwargs) if self.pform is None: form.restore_previous_post(form.session_key()) # assert form.parent_form self.add_hiddens(form, entity) return form def cell_call(self, row, col, i18nctx, **kwargs): """ :param peid: the parent entity's eid hosting the inline form :param rtype: the relation bridging `etype` and `peid` :param role: the role played by the `peid` in the relation """ entity = self._entity() divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % ( self.peid, self.rtype, entity.eid) self.render_form(i18nctx, divonclick=divonclick, **kwargs) def _get_removejs(self): """ Don't display the remove link in edition form if the cardinality is 1. Handled in InlineEntityCreationFormView for creation form. """ entity = self._entity() rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype) card = rdef.role_cardinality(self.role) if card == '1': # don't display remove link return None # if cardinality is 1..n (+), dont display link to remove an inlined form for the first form # allowing to edit the relation. To detect so: # # * if parent form (pform) is None, we're generated through an ajax call and so we know this # is not the first form # # * if parent form is not None, look for previous InlinedFormField in the parent's form # fields if card == '+' and self.pform is not None: # retrieve all field'views handling this relation and return None if we're the first of # them first_view = next( iter((f.view for f in self.pform.fields if isinstance(f, InlinedFormField) and f.view.rtype == self.rtype and f.view.role == self.role))) if self == first_view: return None return self.removejs and self.removejs % (self.peid, self.rtype, entity.eid) def render_form(self, i18nctx, **kwargs): """fetch and render the form""" entity = self._entity() divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid) title = self.form_title(entity, i18nctx) removejs = self._get_removejs() countkey = '%s_count' % self.rtype try: self._cw.data[countkey] += 1 except KeyError: self._cw.data[countkey] = 1 self.form.render(w=self.w, divid=divid, title=title, removejs=removejs, i18nctx=i18nctx, counter=self._cw.data[countkey], **kwargs) def form_title(self, entity, i18nctx): return self._cw.pgettext(i18nctx, entity.cw_etype) def add_hiddens(self, form, entity): """to ease overriding (see cubes.vcsfile.views.forms for instance)""" iid = 'rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid) # * str(self.rtype) in case it's a schema object # * neged_role() since role is the for parent entity, we want the role # of the inlined entity form.add_hidden(name=str(self.rtype), value=self.peid, role=neg_role(self.role), eidparam=True, id=iid) def keep_entity(self, form, entity): if not entity.has_eid(): return True # are we regenerating form because of a validation error? if form.form_previous_values: cdvalues = self._cw.list_form_param( eid_param(self.rtype, self.peid), form.form_previous_values) if str(entity.eid) not in cdvalues: return False return True
class EntityFieldsForm(FieldsForm): """This class is designed for forms used to edit some entities. It should handle for you all the underlying stuff necessary to properly work with the generic :class:`~cubicweb.web.views.editcontroller.EditController`. """ __regid__ = 'base' __select__ = (match_kwargs('entity') | (one_line_rset() & non_final_entity())) domid = 'entityForm' uicfg_aff = uicfg.autoform_field uicfg_affk = uicfg.autoform_field_kwargs @iclassmethod def field_by_name(cls_or_self, name, role=None, eschema=None): """return field with the given name and role. If field is not explicitly defined for the form but `eclass` is specified, guess_field will be called. """ try: return super(EntityFieldsForm, cls_or_self).field_by_name(name, role) except form.FieldNotFound: if eschema is None or role is None or name not in eschema.schema: raise rschema = eschema.schema.rschema(name) # XXX use a sample target type. Document this. tschemas = rschema.targets(eschema, role) fieldclass = cls_or_self.uicfg_aff.etype_get( eschema, rschema, role, tschemas[0]) kwargs = cls_or_self.uicfg_affk.etype_get(eschema, rschema, role, tschemas[0]) if kwargs is None: kwargs = {} if fieldclass: if not isinstance(fieldclass, type): return fieldclass # already an instance kwargs['fieldclass'] = fieldclass if isinstance(cls_or_self, type): req = None else: req = cls_or_self._cw field = guess_field(eschema, rschema, role, req=req, eidparam=True, **kwargs) if field is None: raise return field def __init__(self, _cw, rset=None, row=None, col=None, **kwargs): try: self.edited_entity = kwargs.pop('entity') except KeyError: self.edited_entity = rset.complete_entity(row or 0, col or 0) msg = kwargs.pop('submitmsg', None) super(EntityFieldsForm, self).__init__(_cw, rset, row, col, **kwargs) self.uicfg_aff = self._cw.vreg['uicfg'].select( 'autoform_field', self._cw, entity=self.edited_entity) self.uicfg_affk = self._cw.vreg['uicfg'].select( 'autoform_field_kwargs', self._cw, entity=self.edited_entity) self.add_hidden('__type', self.edited_entity.cw_etype, eidparam=True) self.add_hidden('eid', self.edited_entity.eid) self.add_generation_time() # mainform default to true in parent, hence default to True if kwargs.get('mainform', True) or kwargs.get('mainentity', False): self.add_hidden(u'__maineid', self.edited_entity.eid) # If we need to directly attach the new object to another one if '__linkto' in self._cw.form: if msg: msg = '%s %s' % (msg, self._cw._('and linked')) else: msg = self._cw._('entity linked') if msg: msgid = self._cw.set_redirect_message(msg) self.add_hidden('_cwmsgid', msgid) def add_generation_time(self): # use %f to prevent (unlikely) display in exponential format self.add_hidden('__form_generation_time', '%.6f' % time.time(), eidparam=True) def add_linkto_hidden(self): """add the __linkto hidden field used to directly attach the new object to an existing other one when the relation between those two is not already present in the form. Warning: this method must be called only when all form fields are setup """ for (rtype, role), eids in self.linked_to.items(): # if the relation is already setup by a form field, do not add it # in a __linkto hidden to avoid setting it twice in the controller try: self.field_by_name(rtype, role) except form.FieldNotFound: for eid in eids: self.add_hidden('__linkto', '%s:%s:%s' % (rtype, eid, role)) def render(self, *args, **kwargs): self.add_linkto_hidden() return super(EntityFieldsForm, self).render(*args, **kwargs) @property @cached def linked_to(self): linked_to = {} # case where this is an embeded creation form try: eid = int(self.cw_extra_kwargs['peid']) except (KeyError, ValueError): # When parent is being created, its eid is not numeric (e.g. 'A') # hence ValueError. pass else: ltrtype = self.cw_extra_kwargs['rtype'] ltrole = neg_role(self.cw_extra_kwargs['role']) linked_to[(ltrtype, ltrole)] = [eid] # now consider __linkto if the current form is the main form try: self.field_by_name('__maineid') except form.FieldNotFound: return linked_to for linkto in self._cw.list_form_param('__linkto'): ltrtype, eid, ltrole = linkto.split(':') linked_to.setdefault((ltrtype, ltrole), []).append(int(eid)) return linked_to def session_key(self): """return the key that may be used to store / retreive data about a previous post which failed because of a validation error """ if self.force_session_key is not None: return self.force_session_key # XXX if this is a json request, suppose we should redirect to the # entity primary view if self._cw.ajax_request and self.edited_entity.has_eid(): return '%s#%s' % (self.edited_entity.absolute_url(), self.domid) # XXX we should not consider some url parameters that may lead to # different url after a validation error return '%s#%s' % (self._cw.url(), self.domid) def default_renderer(self): return self._cw.vreg['formrenderers'].select(self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row, col=self.cw_col, entity=self.edited_entity) def should_display_add_new_relation_link(self, rschema, existant, card): return False # controller side method (eg POST reception handling) def actual_eid(self, eid): # should be either an int (existant entity) or a variable (to be # created entity) assert eid or eid == 0, repr(eid) # 0 is a valid eid try: return int(eid) except ValueError: try: return self._cw.data['eidmap'][eid] except KeyError: self._cw.data['eidmap'][eid] = None return None def editable_relations(self): return ()
class AutoClickAndEditFormView(EntityView): __regid__ = 'reledit' __select__ = non_final_entity() & match_kwargs('rtype') # ui side continuations _onclick = ( u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', " "'%(divid)s', %(reload)s, '%(vid)s', '%(action)s');") _cancelclick = "cw.reledit.cleanupAfterCancel('%s')" # ui side actions/buttons _addzone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>' _addmsg = _('click to add a value') _addlogo = 'plus.png' _deletezone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>' _deletemsg = _('click to delete this value') _deletelogo = 'cancel.png' _editzone = u'<img title="%(msg)s" src="%(logo)s" alt="%(msg)s"/>' _editzonemsg = _('click to edit this field') _editlogo = 'pen_icon.png' # renderer _form_renderer_id = 'base' def entity_call( self, entity, rtype=None, role='subject', reload=False, # controls reloading the whole page after change # boolean, eid (to redirect), or # function taking the subject entity & returning a boolean or an eid rvid=None, # vid to be applied to other side of rtype (non final relations only) default_value=None, formid='base', action=None): """display field to edit entity's `rtype` relation on click""" assert rtype self._cw.add_css('cubicweb.form.css') self._cw.add_js( ('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js')) self.entity = entity rschema = self._cw.vreg.schema[rtype] rctrl = self._cw.vreg['uicfg'].select('reledit', self._cw, entity=entity) self._rules = rctrl.etype_get(self.entity.e_schema.type, rschema.type, role, '*') reload = self._compute_reload(rschema, role, reload) divid = self._build_divid(rtype, role, self.entity.eid) if rschema.final: self._handle_attribute(rschema, role, divid, reload, action) else: if self._is_composite(): self._handle_composite(rschema, role, divid, reload, formid, action) else: self._handle_relation(rschema, role, divid, reload, formid, action) def _handle_attribute(self, rschema, role, divid, reload, action): rvid = self._rules.get('rvid', None) if rvid is not None: value = self._cw.view(rvid, entity=self.entity, rtype=rschema.type, role=role) else: value = self.entity.printable_value(rschema.type) if not self._should_edit_attribute(rschema): self.w(value) return form, renderer = self._build_form(self.entity, rschema, role, divid, 'base', reload, action) value = value or self._compute_default_value(rschema, role) self.view_form(divid, value, form, renderer) def _compute_formid_value(self, rschema, role, rvid, formid): related_rset = self.entity.related(rschema.type, role) if related_rset: value = self._cw.view(rvid, related_rset) else: value = self._compute_default_value(rschema, role) if not self._should_edit_relation(rschema, role): return None, value return formid, value def _handle_relation(self, rschema, role, divid, reload, formid, action): rvid = self._rules.get('rvid', 'autolimited') formid, value = self._compute_formid_value(rschema, role, rvid, formid) if formid is None: return self.w(value) form, renderer = self._build_form(self.entity, rschema, role, divid, formid, reload, action, dict(vid=rvid)) self.view_form(divid, value, form, renderer) def _handle_composite(self, rschema, role, divid, reload, formid, action): # this is for attribute-like composites (1 target type, 1 related entity at most, for now) entity = self.entity related_rset = entity.related(rschema.type, role) add_related = self._may_add_related(related_rset, rschema, role) edit_related = self._may_edit_related_entity(related_rset, rschema, role) delete_related = edit_related and self._may_delete_related( related_rset, rschema, role) rvid = self._rules.get('rvid', 'autolimited') formid, value = self._compute_formid_value(rschema, role, rvid, formid) if formid is None or not (edit_related or add_related): # till we learn to handle cases where not (edit_related or add_related) self.w(value) return form, renderer = self._build_form(entity, rschema, role, divid, formid, reload, action, dict(vid=rvid)) self.view_form(divid, value, form, renderer, edit_related, add_related, delete_related) @cached def _compute_ttypes(self, rschema, role): dual_role = neg_role(role) return getattr(rschema, '%ss' % dual_role)() def _compute_reload(self, rschema, role, reload): ctrl_reload = self._rules.get('reload', reload) if callable(ctrl_reload): ctrl_reload = ctrl_reload(self.entity) if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False ctrl_reload = self._cw.build_url(ctrl_reload) return ctrl_reload def _compute_default_value(self, rschema, role): default = self._rules.get('novalue_label') if default is None: if self._rules.get('novalue_include_rtype'): default = self._cw._('<%s not specified>') % display_name( self._cw, rschema.type, role) else: default = self._cw._('<not specified>') else: default = self._cw._(default) return xml_escape(default) def _is_composite(self): return self._rules.get('edit_target') == 'related' def _may_add_related(self, related_rset, rschema, role): """ ok for attribute-like composite entities """ ttypes = self._compute_ttypes(rschema, role) if len(ttypes) > 1: # many etypes: learn how to do it return False rdef = rschema.role_rdef(self.entity.e_schema, ttypes[0], role) card = rdef.role_cardinality(role) if related_rset or card not in '?1': return False if role == 'subject': kwargs = {'fromeid': self.entity.eid} else: kwargs = {'toeid': self.entity.eid} return rdef.has_perm(self._cw, 'add', **kwargs) def _may_edit_related_entity(self, related_rset, rschema, role): """ controls the edition of the related entity """ ttypes = self._compute_ttypes(rschema, role) if len(ttypes) > 1 or len(related_rset.rows) != 1: return False if self.entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': return False return related_rset.get_entity(0, 0).cw_has_perm('update') def _may_delete_related(self, related_rset, rschema, role): # we assume may_edit_related, only 1 related entity if not related_rset: return False rentity = related_rset.get_entity(0, 0) entity = self.entity if role == 'subject': kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid} cardinality = rschema.rdefs[(entity.cw_etype, rentity.cw_etype)].cardinality[0] else: kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid} cardinality = rschema.rdefs[(rentity.cw_etype, entity.cw_etype)].cardinality[1] if cardinality in '1+': return False # NOTE: should be sufficient given a well built schema/security return rschema.has_perm(self._cw, 'delete', **kwargs) def _build_zone(self, zonedef, msg, logo): return zonedef % { 'msg': xml_escape(self._cw._(msg)), 'logo': xml_escape(self._cw.data_url(logo)) } def _build_edit_zone(self): return self._build_zone(self._editzone, self._editzonemsg, self._editlogo) def _build_delete_zone(self): return self._build_zone(self._deletezone, self._deletemsg, self._deletelogo) def _build_add_zone(self): return self._build_zone(self._addzone, self._addmsg, self._addlogo) def _build_divid(self, rtype, role, entity_eid): """ builds an id for the root div of a reledit widget """ return '%s-%s-%s' % (rtype, role, entity_eid) def _build_args(self, entity, rtype, role, formid, reload, action, extradata=None): divid = self._build_divid(rtype, role, entity.eid) event_args = { 'divid': divid, 'eid': entity.eid, 'rtype': rtype, 'formid': formid, 'reload': json_dumps(reload), 'action': action, 'role': role, 'vid': u'' } if extradata: event_args.update(extradata) return event_args def _prepare_form(self, entity, rschema, role, action): assert action in ('edit_rtype', 'edit_related', 'add', 'delete'), action if action == 'edit_rtype': return False, entity label = True if action in ('edit_related', 'delete'): edit_entity = entity.related(rschema, role).get_entity(0, 0) elif action == 'add': add_etype = self._compute_ttypes(rschema, role)[0] _new_entity = self._cw.vreg['etypes'].etype_class(add_etype)( self._cw) _new_entity.eid = next(self._cw.varmaker) edit_entity = _new_entity # XXX see forms.py ~ 276 and entities.linked_to method # is there another way? self._cw.form['__linkto'] = '%s:%s:%s' % (rschema, entity.eid, neg_role(role)) assert edit_entity return label, edit_entity def _build_renderer(self, related_entity, display_label): return self._cw.vreg['formrenderers'].select( self._form_renderer_id, self._cw, entity=related_entity, display_label=display_label, table_class='attributeForm' if display_label else '', display_help=False, button_bar_class='buttonbar', display_progress_div=False) def _build_form(self, entity, rschema, role, divid, formid, reload, action, extradata=None, **formargs): rtype = rschema.type event_args = self._build_args(entity, rtype, role, formid, reload, action, extradata) if not action: form = _DummyForm() form.event_args = event_args return form, None label, edit_entity = self._prepare_form(entity, rschema, role, action) cancelclick = self._cancelclick % divid form = self._cw.vreg['forms'].select( formid, self._cw, rset=edit_entity.as_rset(), entity=edit_entity, domid='%s-form' % divid, formtype='inlined', action=self._cw.build_url( 'validateform', __onsuccess='window.parent.cw.reledit.onSuccess'), cwtarget='eformframe', cssclass='releditForm', **formargs) # pass reledit arguments for pname, pvalue in event_args.items(): form.add_hidden('__reledit|' + pname, pvalue) # handle buttons if form.form_buttons: # edition, delete form_buttons = [] for button in form.form_buttons: if not button.label.endswith('apply'): if button.label.endswith('cancel'): button = copy.deepcopy(button) button.cwaction = None button.onclick = cancelclick if 'class' in button.attrs: new_class = button.attrs['class'].replace( 'cwjs-edition-cancel', '') button.attrs['class'] = new_class form_buttons.append(button) form.form_buttons = form_buttons else: # base form.form_buttons = [ SubmitButton(), Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick) ] form.event_args = event_args if formid == 'base': field = form.field_by_name(rtype, role, entity.e_schema) form.append_field(field) return form, self._build_renderer(edit_entity, label) def _should_edit_attribute(self, rschema): entity = self.entity rdef = entity.e_schema.rdef(rschema) # check permissions if not entity.cw_has_perm('update'): return False rdef = entity.e_schema.rdef(rschema) return rdef.has_perm(self._cw, 'update', eid=entity.eid) def _should_edit_relation(self, rschema, role): eeid = self.entity.eid perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid} return rschema.has_perm(self._cw, 'add', **perm_args) def _open_form_wrapper(self, divid, value, form, renderer, _edit_related, _add_related, _delete_related): w = self.w w( u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' % { 'id': divid, 'css': 'releditField', 'out': "jQuery('#%s').addClass('invisible')" % divid, 'over': "jQuery('#%s').removeClass('invisible')" % divid }) w(u'<div id="%s-value" class="editableFieldValue">' % divid) w(value) w(u'</div>') form.render(w=w, renderer=renderer) w(u'<div id="%s" class="editableField invisible">' % divid) def _edit_action(self, divid, args, edit_related, add_related, _delete_related): # XXX disambiguate wrt edit_related if not add_related: # currently, excludes edition w = self.w args['formid'] = 'edition' if edit_related else 'base' args['action'] = 'edit_related' if edit_related else 'edit_rtype' w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape( self._onclick % args), self._cw._(self._editzonemsg))) w(self._build_edit_zone()) w(u'</div>') def _add_action(self, divid, args, _edit_related, add_related, _delete_related): if add_related: w = self.w args['formid'] = 'edition' args['action'] = 'add' w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape( self._onclick % args), self._cw._(self._addmsg))) w(self._build_add_zone()) w(u'</div>') def _del_action(self, divid, args, _edit_related, _add_related, delete_related): if delete_related: w = self.w args['formid'] = 'deleteconf' args['action'] = 'delete' w(u'<div id="%s-delete" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape( self._onclick % args), self._cw._(self._deletemsg))) w(self._build_delete_zone()) w(u'</div>') def _close_form_wrapper(self): self.w(u'</div>') self.w(u'</div>') def view_form(self, divid, value, form=None, renderer=None, edit_related=False, add_related=False, delete_related=False): self._open_form_wrapper(divid, value, form, renderer, edit_related, add_related, delete_related) args = form.event_args.copy() self._edit_action(divid, args, edit_related, add_related, delete_related) self._add_action(divid, args, edit_related, add_related, delete_related) self._del_action(divid, args, edit_related, add_related, delete_related) self._close_form_wrapper()