|
1 """abstract controler classe for CubicWeb web client |
|
2 |
|
3 |
|
4 :organization: Logilab |
|
5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
7 """ |
|
8 __docformat__ = "restructuredtext en" |
|
9 |
|
10 from mx.DateTime import strptime, Error as MxDTError, TimeDelta |
|
11 |
|
12 from cubicweb import typed_eid |
|
13 from cubicweb.common.registerers import priority_registerer |
|
14 from cubicweb.common.selectors import in_group_selector |
|
15 from cubicweb.common.appobject import AppObject |
|
16 from cubicweb.web import LOGGER, Redirect, RequestError |
|
17 |
|
18 |
|
19 NAVIGATION_PARAMETERS = (('vid', '__redirectvid'), |
|
20 ('rql', '__redirectrql'), |
|
21 ('__redirectpath', '__redirectpath'), |
|
22 ('__redirectparams', '__redirectparams'), |
|
23 ) |
|
24 NAV_FORM_PARAMETERS = [fp for ap, fp in NAVIGATION_PARAMETERS] |
|
25 |
|
26 def redirect_params(form): |
|
27 """transform redirection parameters into navigation parameters |
|
28 """ |
|
29 params = {} |
|
30 # extract navigation parameters from redirection parameters |
|
31 for navparam, redirectparam in NAVIGATION_PARAMETERS: |
|
32 if navparam == redirectparam: |
|
33 continue |
|
34 if redirectparam in form: |
|
35 params[navparam] = form[redirectparam] |
|
36 return params |
|
37 |
|
38 def parse_relations_descr(rdescr): |
|
39 """parse a string describing some relations, in the form |
|
40 subjeids:rtype:objeids |
|
41 where subjeids and objeids are eids separeted by a underscore |
|
42 |
|
43 return an iterator on (subject eid, relation type, object eid) found |
|
44 """ |
|
45 for rstr in rdescr: |
|
46 subjs, rtype, objs = rstr.split(':') |
|
47 for subj in subjs.split('_'): |
|
48 for obj in objs.split('_'): |
|
49 yield typed_eid(subj), rtype, typed_eid(obj) |
|
50 |
|
51 def append_url_params(url, params): |
|
52 """append raw parameters to the url. Given parameters, if any, are expected |
|
53 to be already url-quoted. |
|
54 """ |
|
55 if params: |
|
56 if not '?' in url: |
|
57 url += '?' |
|
58 else: |
|
59 url += '&' |
|
60 url += params |
|
61 return url |
|
62 |
|
63 |
|
64 class Controller(AppObject): |
|
65 """a controller is responsible to make necessary stuff to publish |
|
66 a request. There is usually at least one standard "view" controller |
|
67 and another linked by forms to edit objects ("edit"). |
|
68 """ |
|
69 __registry__ = 'controllers' |
|
70 __registerer__ = priority_registerer |
|
71 __selectors__ = (in_group_selector,) |
|
72 require_groups = () |
|
73 |
|
74 def __init__(self, *args, **kwargs): |
|
75 super(Controller, self).__init__(*args, **kwargs) |
|
76 # attributes use to control after edition redirection |
|
77 self._after_deletion_path = None |
|
78 self._edited_entity = None |
|
79 |
|
80 def publish(self, rset=None): |
|
81 """publish the current request, with an option input rql string |
|
82 (already processed if necessary) |
|
83 """ |
|
84 raise NotImplementedError |
|
85 |
|
86 # generic methods useful for concret implementations ###################### |
|
87 |
|
88 def check_expected_params(self, params): |
|
89 """check that the given list of parameters are specified in the form |
|
90 dictionary |
|
91 """ |
|
92 missing = [] |
|
93 for param in params: |
|
94 if not self.req.form.get(param): |
|
95 missing.append(param) |
|
96 if missing: |
|
97 raise RequestError('missing required parameter(s): %s' |
|
98 % ','.join(missing)) |
|
99 |
|
100 def parse_datetime(self, value, etype='Datetime'): |
|
101 """get a datetime or time from a string (according to etype) |
|
102 Datetime formatted as Date are accepted |
|
103 """ |
|
104 assert etype in ('Datetime', 'Date', 'Time'), etype |
|
105 # XXX raise proper validation error |
|
106 if etype == 'Datetime': |
|
107 format = self.req.property_value('ui.datetime-format') |
|
108 try: |
|
109 return strptime(value, format) |
|
110 except MxDTError: |
|
111 pass |
|
112 elif etype == 'Time': |
|
113 format = self.req.property_value('ui.time-format') |
|
114 try: |
|
115 # (adim) I can't find a way to parse a Time with a custom format |
|
116 date = strptime(value, format) # this returns a DateTime |
|
117 return TimeDelta(date.hour, date.minute, date.second) |
|
118 except MxDTError: |
|
119 raise ValueError('can\'t parse %r (expected %s)' % (value, format)) |
|
120 try: |
|
121 format = self.req.property_value('ui.date-format') |
|
122 return strptime(value, format) |
|
123 except MxDTError: |
|
124 raise ValueError('can\'t parse %r (expected %s)' % (value, format)) |
|
125 |
|
126 |
|
127 def notify_edited(self, entity): |
|
128 """called by edit_entity() to notify which entity is edited""" |
|
129 # NOTE: we can't use entity.rest_path() at this point because |
|
130 # rest_path() could rely on schema constraints (such as a required |
|
131 # relation) that might not be satisfied yet (in case of creations) |
|
132 if not self._edited_entity: |
|
133 self._edited_entity = entity |
|
134 |
|
135 def delete_entities(self, eidtypes): |
|
136 """delete entities from the repository""" |
|
137 redirect_info = set() |
|
138 eidtypes = tuple(eidtypes) |
|
139 for eid, etype in eidtypes: |
|
140 entity = self.req.eid_rset(eid, etype).get_entity(0, 0) |
|
141 path, params = entity.after_deletion_path() |
|
142 redirect_info.add( (path, tuple(params.iteritems())) ) |
|
143 entity.delete() |
|
144 if len(redirect_info) > 1: |
|
145 # In the face of ambiguity, refuse the temptation to guess. |
|
146 self._after_deletion_path = 'view', () |
|
147 else: |
|
148 self._after_deletion_path = iter(redirect_info).next() |
|
149 if len(eidtypes) > 1: |
|
150 self.req.set_message(self.req._('entities deleted')) |
|
151 else: |
|
152 self.req.set_message(self.req._('entity deleted')) |
|
153 |
|
154 def delete_relations(self, rdefs): |
|
155 """delete relations from the repository""" |
|
156 # FIXME convert to using the syntax subject:relation:eids |
|
157 execute = self.req.execute |
|
158 for subj, rtype, obj in rdefs: |
|
159 rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype |
|
160 execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) |
|
161 self.req.set_message(self.req._('relations deleted')) |
|
162 |
|
163 def insert_relations(self, rdefs): |
|
164 """insert relations into the repository""" |
|
165 execute = self.req.execute |
|
166 for subj, rtype, obj in rdefs: |
|
167 rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype |
|
168 execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) |
|
169 |
|
170 |
|
171 def reset(self): |
|
172 """reset form parameters and redirect to a view determinated by given |
|
173 parameters |
|
174 """ |
|
175 newparams = {} |
|
176 # sets message if needed |
|
177 if self.req.message: |
|
178 newparams['__message'] = self.req.message |
|
179 if self.req.form.has_key('__action_apply'): |
|
180 self._return_to_edition_view(newparams) |
|
181 if self.req.form.has_key('__action_cancel'): |
|
182 self._return_to_lastpage(newparams) |
|
183 else: |
|
184 self._return_to_original_view(newparams) |
|
185 |
|
186 |
|
187 def _return_to_original_view(self, newparams): |
|
188 """validate-button case""" |
|
189 # transforms __redirect[*] parameters into regular form parameters |
|
190 newparams.update(redirect_params(self.req.form)) |
|
191 # find out if we have some explicit `rql` needs |
|
192 rql = newparams.pop('rql', None) |
|
193 # if rql is needed (explicit __redirectrql or multiple deletions for |
|
194 # instance), we have to use the old `view?rql=...` form |
|
195 if rql: |
|
196 path = 'view' |
|
197 newparams['rql'] = rql |
|
198 elif '__redirectpath' in self.req.form: |
|
199 # if redirect path was explicitly specified in the form, use it |
|
200 path = self.req.form['__redirectpath'] |
|
201 elif self._after_deletion_path: |
|
202 # else it should have been set during form processing |
|
203 path, params = self._after_deletion_path |
|
204 params = dict(params) # params given as tuple |
|
205 params.update(newparams) |
|
206 newparams = params |
|
207 elif self._edited_entity: |
|
208 path = self._edited_entity.rest_path() |
|
209 else: |
|
210 path = 'view' |
|
211 url = self.build_url(path, **newparams) |
|
212 url = append_url_params(url, self.req.form.get('__redirectparams')) |
|
213 raise Redirect(url) |
|
214 |
|
215 |
|
216 def _return_to_edition_view(self, newparams): |
|
217 """apply-button case""" |
|
218 form = self.req.form |
|
219 if self._edited_entity: |
|
220 path = self._edited_entity.rest_path() |
|
221 newparams.pop('rql', None) |
|
222 # else, fallback on the old `view?rql=...` url form |
|
223 elif 'rql' in self.req.form: |
|
224 path = 'view' |
|
225 newparams['rql'] = form['rql'] |
|
226 else: |
|
227 self.warning("the edited data seems inconsistent") |
|
228 path = 'view' |
|
229 # pick up the correction edition view |
|
230 if form.get('__form_id'): |
|
231 newparams['vid'] = form['__form_id'] |
|
232 # re-insert copy redirection parameters |
|
233 for redirectparam in NAV_FORM_PARAMETERS: |
|
234 if redirectparam in form: |
|
235 newparams[redirectparam] = form[redirectparam] |
|
236 raise Redirect(self.build_url(path, **newparams)) |
|
237 |
|
238 |
|
239 def _return_to_lastpage(self, newparams): |
|
240 """cancel-button case: in this case we are always expecting to go back |
|
241 where we came from, and this is not easy. Currently we suppose that |
|
242 __redirectpath is specifying that place if found, else we look in the |
|
243 request breadcrumbs for the last visited page. |
|
244 """ |
|
245 if '__redirectpath' in self.req.form: |
|
246 # if redirect path was explicitly specified in the form, use it |
|
247 path = self.req.form['__redirectpath'] |
|
248 url = self.build_url(path, **newparams) |
|
249 url = append_url_params(url, self.req.form.get('__redirectparams')) |
|
250 else: |
|
251 url = self.req.last_visited_page() |
|
252 raise Redirect(url) |
|
253 |
|
254 |
|
255 from cubicweb import set_log_methods |
|
256 set_log_methods(Controller, LOGGER) |
|
257 |