|
1 """abstract form classes for CubicWeb web client |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 """ |
|
7 __docformat__ = "restructuredtext en" |
|
8 |
|
9 from simplejson import dumps |
|
10 |
|
11 from logilab.mtconverter import html_escape |
|
12 |
|
13 from cubicweb import typed_eid |
|
14 from cubicweb.common.selectors import req_form_params_selector |
|
15 from cubicweb.common.registerers import accepts_registerer |
|
16 from cubicweb.common.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView |
|
17 from cubicweb.web import stdmsgs |
|
18 from cubicweb.web.httpcache import NoHTTPCacheManager |
|
19 from cubicweb.web.controller import redirect_params |
|
20 |
|
21 |
|
22 def relation_id(eid, rtype, target, reid): |
|
23 if target == 'subject': |
|
24 return u'%s:%s:%s' % (eid, rtype, reid) |
|
25 return u'%s:%s:%s' % (reid, rtype, eid) |
|
26 |
|
27 |
|
28 class FormMixIn(object): |
|
29 """abstract form mix-in""" |
|
30 category = 'form' |
|
31 controller = 'edit' |
|
32 domid = 'entityForm' |
|
33 |
|
34 http_cache_manager = NoHTTPCacheManager |
|
35 add_to_breadcrumbs = False |
|
36 skip_relations = set() |
|
37 |
|
38 def __init__(self, req, rset): |
|
39 super(FormMixIn, self).__init__(req, rset) |
|
40 self.maxrelitems = self.req.property_value('navigation.related-limit') |
|
41 self.maxcomboitems = self.req.property_value('navigation.combobox-limit') |
|
42 self.force_display = not not req.form.get('__force_display') |
|
43 # get validation session data which may have been previously set. |
|
44 # deleting validation errors here breaks form reloading (errors are |
|
45 # no more available), they have to be deleted by application's publish |
|
46 # method on successful commit |
|
47 formurl = req.url() |
|
48 forminfo = req.get_session_data(formurl) |
|
49 if forminfo: |
|
50 req.data['formvalues'] = forminfo['values'] |
|
51 req.data['formerrors'] = errex = forminfo['errors'] |
|
52 req.data['displayederrors'] = set() |
|
53 # if some validation error occured on entity creation, we have to |
|
54 # get the original variable name from its attributed eid |
|
55 foreid = errex.entity |
|
56 for var, eid in forminfo['eidmap'].items(): |
|
57 if foreid == eid: |
|
58 errex.eid = var |
|
59 break |
|
60 else: |
|
61 errex.eid = foreid |
|
62 |
|
63 def html_headers(self): |
|
64 """return a list of html headers (eg something to be inserted between |
|
65 <head> and </head> of the returned page |
|
66 |
|
67 by default forms are neither indexed nor followed |
|
68 """ |
|
69 return [NOINDEX, NOFOLLOW] |
|
70 |
|
71 def linkable(self): |
|
72 """override since forms are usually linked by an action, |
|
73 so we don't want them to be listed by appli.possible_views |
|
74 """ |
|
75 return False |
|
76 |
|
77 @property |
|
78 def limit(self): |
|
79 if self.force_display: |
|
80 return None |
|
81 return self.maxrelitems + 1 |
|
82 |
|
83 def need_multipart(self, entity, categories=('primary', 'secondary')): |
|
84 """return a boolean indicating if form's enctype should be multipart |
|
85 """ |
|
86 for rschema, _, x in entity.relations_by_category(categories): |
|
87 if entity.get_widget(rschema, x).need_multipart: |
|
88 return True |
|
89 # let's find if any of our inlined entities needs multipart |
|
90 for rschema, targettypes, x in entity.relations_by_category('inlineview'): |
|
91 assert len(targettypes) == 1, \ |
|
92 "I'm not able to deal with several targets and inlineview" |
|
93 ttype = targettypes[0] |
|
94 inlined_entity = self.vreg.etype_class(ttype)(self.req, None, None) |
|
95 for irschema, _, x in inlined_entity.relations_by_category(categories): |
|
96 if inlined_entity.get_widget(irschema, x).need_multipart: |
|
97 return True |
|
98 return False |
|
99 |
|
100 def error_message(self): |
|
101 """return formatted error message |
|
102 |
|
103 This method should be called once inlined field errors has been consumed |
|
104 """ |
|
105 errex = self.req.data.get('formerrors') |
|
106 # get extra errors |
|
107 if errex is not None: |
|
108 errormsg = self.req._('please correct the following errors:') |
|
109 displayed = self.req.data['displayederrors'] |
|
110 errors = sorted((field, err) for field, err in errex.errors.items() |
|
111 if not field in displayed) |
|
112 if errors: |
|
113 if len(errors) > 1: |
|
114 templstr = '<li>%s</li>\n' |
|
115 else: |
|
116 templstr = ' %s\n' |
|
117 for field, err in errors: |
|
118 if field is None: |
|
119 errormsg += templstr % err |
|
120 else: |
|
121 errormsg += templstr % '%s: %s' % (self.req._(field), err) |
|
122 if len(errors) > 1: |
|
123 errormsg = '<ul>%s</ul>' % errormsg |
|
124 return u'<div class="errorMessage">%s</div>' % errormsg |
|
125 return u'' |
|
126 |
|
127 def restore_pending_inserts(self, entity, cell=False): |
|
128 """used to restore edition page as it was before clicking on |
|
129 'search for <some entity type>' |
|
130 |
|
131 """ |
|
132 eid = entity.eid |
|
133 cell = cell and "div_insert_" or "tr" |
|
134 pending_inserts = set(self.req.get_pending_inserts(eid)) |
|
135 for pendingid in pending_inserts: |
|
136 eidfrom, rtype, eidto = pendingid.split(':') |
|
137 if typed_eid(eidfrom) == entity.eid: # subject |
|
138 label = display_name(self.req, rtype, 'subject') |
|
139 reid = eidto |
|
140 else: |
|
141 label = display_name(self.req, rtype, 'object') |
|
142 reid = eidfrom |
|
143 jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \ |
|
144 % (pendingid, cell, eid) |
|
145 rset = self.req.eid_rset(reid) |
|
146 eview = self.view('text', rset, row=0) |
|
147 # XXX find a clean way to handle baskets |
|
148 if rset.description[0][0] == 'Basket': |
|
149 eview = '%s (%s)' % (eview, display_name(self.req, 'Basket')) |
|
150 yield rtype, pendingid, jscall, label, reid, eview |
|
151 |
|
152 |
|
153 def force_display_link(self): |
|
154 return (u'<span class="invisible">' |
|
155 u'[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]' |
|
156 u'</span>' % self.req._('view all')) |
|
157 |
|
158 def relations_table(self, entity): |
|
159 """yiels 3-tuples (rtype, target, related_list) |
|
160 where <related_list> itself a list of : |
|
161 - node_id (will be the entity element's DOM id) |
|
162 - appropriate javascript's togglePendingDelete() function call |
|
163 - status 'pendingdelete' or '' |
|
164 - oneline view of related entity |
|
165 """ |
|
166 eid = entity.eid |
|
167 pending_deletes = self.req.get_pending_deletes(eid) |
|
168 # XXX (adim) : quick fix to get Folder relations |
|
169 for label, rschema, target in entity.srelations_by_category(('generic', 'metadata'), 'add'): |
|
170 if rschema in self.skip_relations: |
|
171 continue |
|
172 relatedrset = entity.related(rschema, target, limit=self.limit) |
|
173 toggable_rel_link = self.toggable_relation_link_func(rschema) |
|
174 related = [] |
|
175 for row in xrange(relatedrset.rowcount): |
|
176 nodeid = relation_id(eid, rschema, target, relatedrset[row][0]) |
|
177 if nodeid in pending_deletes: |
|
178 status = u'pendingDelete' |
|
179 label = '+' |
|
180 else: |
|
181 status = u'' |
|
182 label = 'x' |
|
183 dellink = toggable_rel_link(eid, nodeid, label) |
|
184 eview = self.view('oneline', relatedrset, row=row) |
|
185 related.append((nodeid, dellink, status, eview)) |
|
186 yield (rschema, target, related) |
|
187 |
|
188 def toggable_relation_link_func(self, rschema): |
|
189 if not rschema.has_perm(self.req, 'delete'): |
|
190 return lambda x, y, z: u'' |
|
191 return toggable_relation_link |
|
192 |
|
193 |
|
194 def redirect_url(self, entity=None): |
|
195 """return a url to use as next direction if there are some information |
|
196 specified in current form params, else return the result the reset_url |
|
197 method which should be defined in concrete classes |
|
198 """ |
|
199 rparams = redirect_params(self.req.form) |
|
200 if rparams: |
|
201 return self.build_url('view', **rparams) |
|
202 return self.reset_url(entity) |
|
203 |
|
204 def reset_url(self, entity): |
|
205 raise NotImplementedError('implement me in concrete classes') |
|
206 |
|
207 BUTTON_STR = u'<input class="validateButton" type="submit" name="%s" value="%s" tabindex="%s"/>' |
|
208 ACTION_SUBMIT_STR = u'<input class="validateButton" type="button" onclick="postForm(\'%s\', \'%s\', \'%s\')" value="%s" tabindex="%s"/>' |
|
209 |
|
210 def button_ok(self, label=None, tabindex=None): |
|
211 label = self.req._(label or stdmsgs.BUTTON_OK).capitalize() |
|
212 return self.BUTTON_STR % ('defaultsubmit', label, tabindex or 2) |
|
213 |
|
214 def button_apply(self, label=None, tabindex=None): |
|
215 label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize() |
|
216 return self.ACTION_SUBMIT_STR % ('__action_apply', label, self.domid, label, tabindex or 3) |
|
217 |
|
218 def button_delete(self, label=None, tabindex=None): |
|
219 label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize() |
|
220 return self.ACTION_SUBMIT_STR % ('__action_delete', label, self.domid, label, tabindex or 3) |
|
221 |
|
222 def button_cancel(self, label=None, tabindex=None): |
|
223 label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() |
|
224 return self.ACTION_SUBMIT_STR % ('__action_cancel', label, self.domid, label, tabindex or 4) |
|
225 |
|
226 def button_reset(self, label=None, tabindex=None): |
|
227 label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() |
|
228 return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % ( |
|
229 label, tabindex or 4) |
|
230 |
|
231 def toggable_relation_link(eid, nodeid, label='x'): |
|
232 js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid))) |
|
233 return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label) |
|
234 |
|
235 |
|
236 class Form(FormMixIn, View): |
|
237 """base class for forms. Apply by default according to request form |
|
238 parameters specified using the `form_params` class attribute which |
|
239 should list necessary parameters in the form to be accepted. |
|
240 """ |
|
241 __registerer__ = accepts_registerer |
|
242 __select__ = classmethod(req_form_params_selector) |
|
243 |
|
244 form_params = () |
|
245 |
|
246 class EntityForm(FormMixIn, EntityView): |
|
247 """base class for forms applying on an entity (i.e. uniform result set) |
|
248 """ |
|
249 |
|
250 class AnyRsetForm(FormMixIn, AnyRsetView): |
|
251 """base class for forms applying on any empty result sets |
|
252 """ |
|
253 |