Merge lp:~ptressel/sahana-eden/devel into lp:sahana-eden
- devel
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~ptressel/sahana-eden/devel |
Merge into: | lp:sahana-eden |
Diff against target: |
709 lines (+444/-54) 4 files modified
controllers/hrm.py (+6/-0) models/06_hrm.py (+8/-1) modules/s3/s3crud.py (+174/-52) modules/s3/s3tools.py (+256/-1) |
To merge this branch: | bzr merge lp:~ptressel/sahana-eden/devel |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
nursix (community) | Disapprove | ||
Fran Boon | Pending | ||
Review via email:
|
Commit message
Description of the change
There are two example fields added to the hrm_human_resource list_fields, which makes those lines a bit long. So we might want to find someplace else to add fake virtual fields as an example.
Seems like having an official report_fields would be good -- people will likely want different (i.e. more) fields in reports than in list views.
I saw someone ask a question about naming conventions for globals -- is there an Official Change? Because there might be some violations of whatever the new rule is. There are several staticmethod functions in S3VirtualField that might be violations. And there's a method _get_field that could be shared -- I see the same sort of thing being done elsewhere. I gave get_list_fields a _ on front because it's really not appropriate for outside callers -- too non-general.
There are some ToDos. Main one is adding support for fake virtual fields in exporter.xls. If we add report_fields, and switch that over the exporter call to using report_fields, and don't put virtual fields in report_fields yet, we can kick the can down the road a bit.

nursix (nursix.org) wrote : | # |
All in all a brave, but good solution.
Could you please remove/shorten the comments by what does not directly relate to the actual code, but is actually discussing theoretical backgrounds or design options (e.g. the introduction of the S3VirtualField class)?
These comments are very hard too parse, are focussed on working *on* (rather than *with*) the code, and all discussion should better happen on either ML or wiki (preferrably the latter if the design, like in this case, needs some deeper understanding of the background).
There is no issue whatsoever with the naming of the functions inside the S3VirtualField class, don't worry.

nursix (nursix.org) wrote : | # |
1) I do not see a need to declare the virtual fields by name, they can actually be added directly (i.e. as S3VirtualField instances) to the list_fields array. This would also remove the need for the field lookup loop in _get_list_fields.
2) I totally agree that report types like XLS or PDF need a separate array, which can also include S3VirtualFields. Consequently, _get_list_fields should be made generic over any type of field lists, and probably be moved into S3Method (nothing you need to fix for merge, just to answer the question you brought up).
3) Authorization of access to referenced tables (in both fk$fn fields as well as virtual fields) is still missing. This is not quite a new issue, but with this implementation it becomes resolvable.
4) We are creating ambiguity here - "virtual fields" (=web2py) vs. "virtual fields" (=S3VirtualField). We should probably rename into something like "S3ComputedValue" or "S3DerivedValue" to prevent any confusion.
All in all this would give a clear declaration like:
list_
Of course, you can also declare the computed values into variables and then just use the variables in the list_fields setting. Still, the virtual_fields declaration and the respective look-up can be avoided.
Can you fix it like that? Otherwise I'll do.

Pat Tressel (ptressel) wrote : | # |
Dominic --
Sorry about the delay -- I'm just getting a new machine set up... I arrived
at RHoK Saturday morning, turned on my laptop, and...nothing. Not even the
boot loader screen. While they were having the meet'n'greet, and deciding
who was working on what project, I made a dash down to Fry's Electronics
(huge electronics store) and came back with another HP laptop. The old one
lasted ~6 years, and was gradually dying, so not failure was not
unexpected...just really really bad timing. So all I'm set up to do at the
moment is Android app development. Don't even have working security
software or backup yet. So, gimme a little while to either get my backup
running so I can recover files from backup, or get an enclosure to put my
old laptop's disk in... Fortunately, my laptop was on the night before it
failed, and I made no changes after backup ran, so (if either my backup or
the laptop's drive works) I didn't lose anything.
-- Pat

nursix (nursix.org) wrote : | # |
If necessary, re-implement this in a more recent version of CRUD.
Basically that would mean to just extend S3CRUD.

Pat Tressel (ptressel) wrote : | # |
Dominic --
I assumed this was dead due to lack of interest -- seemed I was the only one
who had a use for arbitrary computations to be shown in lists, and I'm sure
the thing I wanted it for, which was something to do with regions /
incidents, is long obsolete.
-- Pat
On Fri, Sep 2, 2011 at 4:43 AM, nursix <email address hidden> wrote:
> Review: Disapprove
> If necessary, re-implement this in a more recent version of CRUD.
>
> Basically that would mean to just extend S3CRUD.
> the extra field pre-loading is already implemented. The S3VirtualField class
> doesn't seem to be necessary anymore.
>
> --
> https:/
> You are the owner of lp:~ptressel/sahana-eden/devel.
>
Unmerged revisions
- 2266. By Pat Tressel
-
Merge from trunk.
- 2265. By Pat Tressel
-
Add fake virtual fields. For fk$fn for list_fields, allow records with empty fk if fk field isn't notnull.
Preview Diff
1 | === modified file 'controllers/hrm.py' |
2 | --- controllers/hrm.py 2011-05-30 18:34:57 +0000 |
3 | +++ controllers/hrm.py 2011-06-03 10:31:33 +0000 |
4 | @@ -287,6 +287,8 @@ |
5 | s3mgr.configure(tablename, |
6 | list_fields = ["id", |
7 | "person_id", |
8 | + "email", |
9 | + "mobile", |
10 | "person_id$occupation", |
11 | "organisation_id", |
12 | "status", |
13 | @@ -482,6 +484,8 @@ |
14 | if session.hrm.mode is not None: |
15 | s3mgr.configure(tablename, |
16 | list_fields=["id", |
17 | + "email", |
18 | + "mobile", |
19 | "organisation_id", |
20 | "type", |
21 | "job_title", |
22 | @@ -492,6 +496,8 @@ |
23 | else: |
24 | s3mgr.configure(tablename, |
25 | list_fields=["id", |
26 | + "email", |
27 | + "mobile", |
28 | "type", |
29 | "job_title", |
30 | "status", |
31 | |
32 | === modified file 'models/06_hrm.py' |
33 | --- models/06_hrm.py 2011-06-01 21:10:57 +0000 |
34 | +++ models/06_hrm.py 2011-06-03 10:31:33 +0000 |
35 | @@ -443,12 +443,19 @@ |
36 | hrm_update_staff_role(record, user_id, role_id) |
37 | hrm_update_manager_role(record, user_id) |
38 | |
39 | + hrm_email = s3base.S3VirtualFieldPersonContact( |
40 | + db, tablename, "email", T("Email"), 1) |
41 | + |
42 | + hrm_mobile = s3base.S3VirtualFieldPersonContact( |
43 | + db, tablename, "mobile", T("Mobile phone"), 2) |
44 | + |
45 | s3mgr.configure(tablename, |
46 | super_entity = db.sit_trackable, |
47 | deletable = False, |
48 | search_method = human_resource_search, |
49 | onaccept = hrm_human_resource_onaccept, |
50 | - ondelete = hrm_human_resource_ondelete) |
51 | + ondelete = hrm_human_resource_ondelete, |
52 | + virtual_fields = [hrm_email, hrm_mobile]) |
53 | |
54 | # Add Staff as component of Organisations & Incidents |
55 | joinby = dict(org_organisation="organisation_id") |
56 | |
57 | === modified file 'modules/s3/s3crud.py' |
58 | --- modules/s3/s3crud.py 2011-05-30 16:00:35 +0000 |
59 | +++ modules/s3/s3crud.py 2011-06-03 10:31:33 +0000 |
60 | @@ -58,7 +58,7 @@ |
61 | from s3pdf import S3PDF |
62 | |
63 | from gluon.sqlhtml import SQLFORM |
64 | -from s3tools import SQLTABLES3 |
65 | +from s3tools import SQLTABLES3, S3VirtualField |
66 | from s3utils import s3_mark_required |
67 | |
68 | # ***************************************************************************** |
69 | @@ -364,17 +364,6 @@ |
70 | |
71 | editable = self._config("editable", True) |
72 | deletable = self._config("deletable", True) |
73 | - list_fields = self._config("list_fields") |
74 | - |
75 | - # List fields |
76 | - if not list_fields: |
77 | - fields = resource.readable_fields() |
78 | - else: |
79 | - fields = [table[f] for f in list_fields if f in table.fields] |
80 | - if not fields: |
81 | - fields = [] |
82 | - if fields[0].name != table.fields[0]: |
83 | - fields.insert(0, table[table.fields[0]]) |
84 | |
85 | # Get the target record ID |
86 | record_id = self._record_id(r) |
87 | @@ -479,7 +468,18 @@ |
88 | return exporter(r, **attr) |
89 | |
90 | elif representation == "xls": |
91 | - list_fields = self._config("list_fields") |
92 | + # @ToDo: list_fields isn't really intended for reports -- it's |
93 | + # for letting a user see which record they want to open. And most |
94 | + # list_fields have "id" in them, which we likely don't want, unless |
95 | + # represent is applied. Fran says Grameme has put in a |
96 | + # "report_fields", so maybe that would be more appropriate, both |
97 | + # here and for pdf. |
98 | + # @ToDo: Do we want to include indirect or generated fields like |
99 | + # fk$fn or virtual fields? Probably yes, but omitting them for |
100 | + # the moment. (Note cannot pass list_fields in to readable_fields() |
101 | + # when we want to keep the non-table fields.) |
102 | + fields = self.resource.readable_fields(self._config("list_fields")) |
103 | + list_fields = [f.name for f in fields] |
104 | exporter = resource.exporter.xls |
105 | return exporter(resource, list_fields=list_fields) |
106 | |
107 | @@ -790,15 +790,22 @@ |
108 | # List fields |
109 | if not list_fields: |
110 | fields = self.resource.readable_fields() |
111 | - else: |
112 | - fields = [table[f] for f in list_fields if f in table.fields] |
113 | - if not fields: |
114 | - fields = [] |
115 | - |
116 | - if fields[0].name != table.fields[0]: |
117 | - fields.insert(0, table[table.fields[0]]) |
118 | - |
119 | - list_fields = [f.name for f in fields] |
120 | + list_fields = [f.name for f in fields] |
121 | + #else: |
122 | + # @ToDo: Does this not prevent list fields of the form fk$fn |
123 | + # from getting through? In any case, _get_list_fields will take |
124 | + # care of stripping out bogus fields (well, sort of -- see "other" |
125 | + # in _get_list_fields)...and surely including a bogus field is a |
126 | + # bug, and should cause an error rather than silently passing by. |
127 | + # We're only doing this to insert "id" in front if it's not there. |
128 | + #fields = [table[f] for f in list_fields if f in table.fields] |
129 | + #if not fields: |
130 | + #fields = [] |
131 | + |
132 | + field0 = table.fields[0] |
133 | + # Make sure there's an "id" column on the front. |
134 | + if list_fields[0] != field0: |
135 | + list_fields.insert(0, field0) |
136 | |
137 | # Filter |
138 | if response.s3.filter is not None: |
139 | @@ -821,7 +828,7 @@ |
140 | del vars["iSortCol_0"] |
141 | del vars["sSortDir_0"] |
142 | if r.method == "search" and not orderby: |
143 | - orderby = fields[0] |
144 | + orderby = field0 |
145 | |
146 | # Custom view |
147 | response.view = self._view(r, "list.html") |
148 | @@ -862,7 +869,7 @@ |
149 | |
150 | # In SSPag, send the first 20 records together with the initial |
151 | # response (avoids the dataTables Ajax request unless the user |
152 | - # tries nagivating around) |
153 | + # tries navigating around) |
154 | if not response.s3.no_sspag and items: |
155 | totalrows = self.resource.count() |
156 | if totalrows: |
157 | @@ -943,7 +950,7 @@ |
158 | if vars.iSortingCols and orderby is None: |
159 | orderby = self.ssp_orderby(table, list_fields, left=left) |
160 | if r.method == "search" and not orderby: |
161 | - orderby = fields[0] |
162 | + orderby = field0 |
163 | |
164 | # Echo |
165 | sEcho = int(vars.sEcho or 0) |
166 | @@ -987,6 +994,8 @@ |
167 | |
168 | elif representation == "xls": |
169 | exporter = S3Exporter(self.manager) |
170 | + # @ToDo: Add support for fk$fn and virtual fields in exporter.xls. |
171 | + # @ToDo: Use "report_fields" instead of "list_fields". |
172 | return exporter.xls(self.resource, |
173 | list_fields=list_fields, |
174 | report_groupby=report_groupby) |
175 | @@ -1039,7 +1048,12 @@ |
176 | if table._id.name not in fields: |
177 | fields.insert(0, table._id.name) |
178 | |
179 | - lfields, joins = self.get_list_fields(table, fields) |
180 | + lfields, joins, lefts, xfields, vfields = self._get_list_fields(table, fields) |
181 | + if left is None: |
182 | + left = [] |
183 | + elif not isinstance(left, (tuple, list)): |
184 | + left = [left] |
185 | + self._combine_lefts(left, lefts.values()) |
186 | colnames = [f.colname for f in lfields] |
187 | headers = dict(map(lambda f: (f.colname, f.label), lfields)) |
188 | |
189 | @@ -1057,11 +1071,14 @@ |
190 | for j in joins.values(): |
191 | query &= j |
192 | # Left outer joins |
193 | - if left is not None: |
194 | + if left: |
195 | attributes.update(left=left) |
196 | |
197 | # Retrieve the rows |
198 | - qfields = [f.field for f in lfields if f.field is not None] |
199 | + qfields = [f.field for f in lfields if \ |
200 | + f.field is not None and f.type != "s3_virtual_field"] |
201 | + # Include extra fields requested for virtual fields. |
202 | + qfields.extend(xfields) |
203 | rows = db(query).select(*qfields, **attributes) |
204 | if not rows: |
205 | return None |
206 | @@ -1070,9 +1087,12 @@ |
207 | # Representation |
208 | def represent(f, row): |
209 | if f.field: |
210 | - return self.manager.represent(f.field, |
211 | - record=row, |
212 | - linkto=linkto) |
213 | + if isinstance(f.field, S3VirtualField): |
214 | + return f.field.represent(row) |
215 | + else: |
216 | + return self.manager.represent(f.field, |
217 | + record=row, |
218 | + linkto=linkto) |
219 | else: |
220 | if f.colname in row: |
221 | return row[f.colname] |
222 | @@ -1083,17 +1103,20 @@ |
223 | if as_page: |
224 | # ...JSON page (for pagination) |
225 | items = [[represent(f, row) for f in lfields] for row in rows] |
226 | - elif as_list: |
227 | - # ...Python list |
228 | - items = rows.as_list() |
229 | else: |
230 | - # ...SQLTABLE |
231 | - items = SQLTABLES3(rows, |
232 | - headers=headers, |
233 | - linkto=linkto, |
234 | - upload=download_url, |
235 | - _id="list", |
236 | - _class="dataTable display") |
237 | + if vfields: |
238 | + S3VirtualField.add_virtual_values_to_rows(rows, *vfields) |
239 | + if as_list: |
240 | + # ...Python list |
241 | + items = rows.as_list() |
242 | + else: |
243 | + # ...SQLTABLE |
244 | + items = SQLTABLES3(rows, |
245 | + headers=headers, |
246 | + linkto=linkto, |
247 | + upload=download_url, |
248 | + _id="list", |
249 | + _class="dataTable display") |
250 | return items |
251 | |
252 | # ------------------------------------------------------------------------- |
253 | @@ -1583,7 +1606,13 @@ |
254 | wildcard = "%%%s%%" % context |
255 | |
256 | # Retrieve the list of search fields |
257 | - lfields, joins = self.get_list_fields(table, fields) |
258 | + lfields, joins, lefts, xfields, vfields = self._get_list_fields(table, fields) |
259 | + # @ToDo: joins are not used -- are they relevant here? |
260 | + if left is None: |
261 | + left = [] |
262 | + elif not isinstance(left, (tuple, list)): |
263 | + left = [left] |
264 | + self._combine_lefts(left, lefts.values()) |
265 | flist = [] |
266 | for i in xrange(0, columns): |
267 | field = lfields[i].field |
268 | @@ -1670,7 +1699,12 @@ |
269 | |
270 | orderby = [] |
271 | |
272 | - lfields, joins = self.get_list_fields(table, fields) |
273 | + lfields, joins, lefts, xfields, vfields = self._get_list_fields(table, fields) |
274 | + if left is None: |
275 | + left = [] |
276 | + elif not isinstance(left, (tuple, list)): |
277 | + left = [left] |
278 | + self._combine_lefts(left, lefts.values()) |
279 | columns = [lfields[int(vars["iSortCol_%s" % str(i)])].field |
280 | for i in xrange(iSortingCols)] |
281 | for i in xrange(len(columns)): |
282 | @@ -1708,18 +1742,51 @@ |
283 | return ", ".join(orderby) |
284 | |
285 | # ------------------------------------------------------------------------- |
286 | - def get_list_fields(self, table, fields): |
287 | + def _get_list_fields(self, table, fields, include_virtual=True): |
288 | """ |
289 | Helper to resolve list_fields |
290 | |
291 | @param table: the table |
292 | @param fields: the list_fields array |
293 | + @param include_virtual: set this to False to omit virtual fields |
294 | + sqltable.represent() called. |
295 | + |
296 | + #returns: (lfields, joins, lefts, xfields, vfields) where |
297 | + lfields is a list of Storage(), one per item in list_fields, |
298 | + containing: |
299 | + fieldname from list_fields, |
300 | + colname for Rows.colname, |
301 | + field -- the Field object, |
302 | + label, |
303 | + type is "field" for a normal field, or "s3_virtual_field" |
304 | + for a virtual field. |
305 | + joins is a dict of foreign key fieldname vs. associated joins |
306 | + required by fk$fn items in list_fields. This includes an |
307 | + equijoin for notnull foreign key references and a test for |
308 | + not deleted if the table has that field. |
309 | + lefts is a dict of foreign key fieldname vs. left outer joins |
310 | + for foreign key references that are *not* notnull, as |
311 | + needed by fk$fn items |
312 | + xfields is a list of "extra" fields requested by any virtual |
313 | + fields |
314 | + vfields is a list of S3VirtualField instances for the virtual |
315 | + fields on list_fields |
316 | + |
317 | + The caller should merge joins and lefts with any they already have. |
318 | """ |
319 | |
320 | db = self.db |
321 | + model = self.manager.model |
322 | + table_vfields = dict(map(lambda f: (f.name, f), |
323 | + model.get_config(table._tablename, |
324 | + "virtual_fields", []))) |
325 | |
326 | - joins = dict() |
327 | + joins = {} |
328 | lfields = [] |
329 | + lfieldnames = [] |
330 | + lefts = {} |
331 | + xfields = [] # Non-displayed fields that should be included in select. |
332 | + vfields = [] # S3VirtualField instances for any that are in fields. |
333 | for f in fields: |
334 | # Allow to override the field label |
335 | if isinstance(f, tuple): |
336 | @@ -1729,6 +1796,11 @@ |
337 | field = None |
338 | if "$" in fieldname: |
339 | # Field in referenced table |
340 | + # If the foreign key field is notnull, we don't want to restrict |
341 | + # to records that have the field filled in. This same choice is |
342 | + # made elsewhere that a join is added for an fk ref, so we don't |
343 | + # have to deal with fixing up an inconsistent choice. |
344 | + type = "field" |
345 | fk, fn = fieldname.split("$", 1) |
346 | if fk in table.fields: |
347 | ftype = str(table[fk].type) |
348 | @@ -1736,11 +1808,21 @@ |
349 | ftable = db[ftype[10:]] |
350 | if fn in ftable.fields: |
351 | field = ftable[fn] |
352 | - if fk not in joins: |
353 | + if fk not in joins and fk not in lefts: |
354 | join = (table[fk] == ftable._id) |
355 | + query = None |
356 | + if not field.notnull: |
357 | + lefts[fk] = ftable.on(join) |
358 | + else: |
359 | + query = join |
360 | if "deleted" in ftable.fields: |
361 | - join &= (ftable.deleted != True) |
362 | - joins[fk] = join |
363 | + deleted = (ftable.deleted != True) |
364 | + if query: |
365 | + query &= deleted |
366 | + else: |
367 | + query = deleted |
368 | + if query: |
369 | + joins[fk] = query |
370 | if field is None: |
371 | continue |
372 | if label is None: |
373 | @@ -1748,20 +1830,60 @@ |
374 | colname = str(field) |
375 | elif fieldname in table.fields: |
376 | # Field in this table |
377 | + type = "field" |
378 | field = table[fieldname] |
379 | if label is None: |
380 | label = field.label |
381 | colname = str(field) |
382 | + elif table_vfields and fieldname in table_vfields: |
383 | + if not include_virtual: continue |
384 | + # These are our virtual field substitutes. |
385 | + type = "s3_virtual_field" |
386 | + field = table_vfields[fieldname] |
387 | + vfields.append(field) |
388 | + if label is None: |
389 | + label = field.label |
390 | + colname = fieldname |
391 | + if field.required_fields: |
392 | + # @ToDo: Want to let a virtual field include an fk$fn |
393 | + # reference? e.g. pull out above fk$fn join code into a |
394 | + # helper. Note that won't remove the need for some |
395 | + # virtual fields to do additional queries. |
396 | + xfields.extend([table[x] for x in field.required_fields]) |
397 | else: |
398 | - # Virtual field? |
399 | + # @ToDo: Add support for web2py virtual field? |
400 | + type = "other" |
401 | colname = "%s.%s" % (table._tablename, fieldname) |
402 | |
403 | lfields.append(Storage(fieldname = fieldname, |
404 | colname = colname, |
405 | field = field, |
406 | - label = label)) |
407 | - |
408 | - return (lfields, joins) |
409 | + label = label, |
410 | + type = type)) |
411 | + lfieldnames.append(fieldname) |
412 | + |
413 | + # Strip any fields out of xfields that are already in lfields and |
414 | + # remove duplicates. |
415 | + seen = set() |
416 | + xfields = [f for f in xfields if f.name not in lfieldnames and \ |
417 | + f.name not in seen and \ |
418 | + not seen.add(f.name)] |
419 | + return (lfields, joins, lefts, xfields, vfields) |
420 | + |
421 | + # ------------------------------------------------------------------------- |
422 | + def _combine_lefts(self, oldlefts, newlefts): |
423 | + """ |
424 | + Combine two lists of left outer joins without adding duplicates. |
425 | + Results end up in oldlefts. |
426 | + """ |
427 | + |
428 | + # Web2py reorders the arguments in Table.on() so the order is the same |
429 | + # in the resulting Expression, regardless of the order in the on() call. |
430 | + # Comparing str() of the left outer join Expressions will determine if |
431 | + # they are the same join. |
432 | + oldleftstrs = [str(o) for o in oldlefts] |
433 | + morelefts = [n for n in newlefts if str(n) not in oldleftstrs] |
434 | + oldlefts.extend(morelefts) |
435 | |
436 | # END |
437 | # ***************************************************************************** |
438 | |
439 | === modified file 'modules/s3/s3tools.py' |
440 | --- modules/s3/s3tools.py 2011-06-02 11:08:10 +0000 |
441 | +++ modules/s3/s3tools.py 2011-06-03 10:31:33 +0000 |
442 | @@ -39,7 +39,9 @@ |
443 | "QueryS3", |
444 | "FieldS3", |
445 | "CrudS3", |
446 | - "S3ReusableField"] |
447 | + "S3ReusableField", |
448 | + "S3VirtualField", |
449 | + "S3VirtualFieldPersonContact"] |
450 | |
451 | import sys |
452 | import datetime |
453 | @@ -550,3 +552,256 @@ |
454 | |
455 | |
456 | # ============================================================================= |
457 | +class S3VirtualField(object): |
458 | + """ |
459 | + Alternative to Web2py virtual field that is easier to use in a list. |
460 | + |
461 | + Unlike a Web2py virtual field, this one has a name, only provides |
462 | + one function, and knows which fields it needs from its associated |
463 | + table / resource. So it doesn't need reflection to find out what |
464 | + fake fields it represents, and it doesn't require the list code to |
465 | + fetch entire rows because we don't know what fields it might want |
466 | + (bad for tables with big fields like wkt). |
467 | + |
468 | + If desired, these can be stored in the associated resource, so they |
469 | + can be referenced by name in list_fields rather than inserting the |
470 | + instance. |
471 | + |
472 | + Example: |
473 | + |
474 | + def get_email(xself, row, db): |
475 | + pid = xself._get_field(row, "person_id") |
476 | + query = (db.pr_person.id == pid) & \ |
477 | + (db.pr_person.pe_id == db.pr_contact.pe_id) & \ |
478 | + (db.pr_contact.contact_method == 1) |
479 | + result = db(query).select(db.pr_contact.value, limitby=(0,1)).first() |
480 | + if result: |
481 | + return result.value |
482 | + else: |
483 | + return NONE |
484 | + |
485 | + hrm_email = s3base.S3VirtualField(db, |
486 | + "hrm_human_resource", |
487 | + "email", |
488 | + "Email", |
489 | + get_email, |
490 | + ["person_id"]) |
491 | + |
492 | + s3mgr.configure(db.hrm_human_resource, |
493 | + virtual_fields = [hrm_email, ...], |
494 | + list_fields = ["id", ..., "email", ...]) |
495 | + |
496 | + Alternatively, they can be called stand-alone to generate their value, |
497 | + given the id of a record in the associated table: |
498 | + |
499 | + # hr_id is an id in hrm_human_resource |
500 | + email_value = hrm_email.represent(hr_id) |
501 | + |
502 | + Like a Web2py virtual field, the specified function might still do |
503 | + a query to get joined data, so it is not as efficient as an explicit |
504 | + SQL query to fetch the list fields plus all needed joined values. |
505 | + Note it's tricky to specify a query in Web2py query syntax that |
506 | + would return all needed fields *and* all needed rows including those |
507 | + that fail to have a value for the virtual field -- that needs a |
508 | + one-sided outer join. The problem is that list can have a filter, and |
509 | + it's the filtered set that would need to be outer-joined against, not |
510 | + the full table. That can be done in SQL with a nested query / |
511 | + temporary table, but Web2py query syntax, or its "left" outer join |
512 | + option, do not support a nested query / temporary table. However, |
513 | + nothing is stopping the supplied function from looking in the Row |
514 | + it's given to see if the fields it wants from other tables are there. |
515 | + This would require knowing the possible contexts in which it might be |
516 | + called, to be sure that those fields were joined in the intended |
517 | + manner. Since we're writing the function, and putting in the calls to |
518 | + it, we do know if opportunistic use of scavenged fields is ok. But |
519 | + it's safer to just do a query and fetch them... For some sorts of |
520 | + fields from other tables, such as those with a direct foreign key |
521 | + reference from the primary table, or components of this resource, we |
522 | + could specify the needed fields. But that just pushes the need to do |
523 | + an outer join against the filtered set upstream. |
524 | + |
525 | + @ToDo: If we could make this behave enough like a Field not to confuse |
526 | + whatever Field-like operations are needed, we could subclass Field. |
527 | + Goal would be to put these directly in Table (or a Table subclass), |
528 | + where they could be ignored for db operations, but used for display |
529 | + in forms. |
530 | + """ |
531 | + |
532 | + def __init__(self, db, tablename, fieldname, label, method, |
533 | + required_fields, **attr): |
534 | + """ |
535 | + @param db: A DAL instance. |
536 | + @param tablename: The name of the table with which this "field" is |
537 | + associated. (Needed to dereference values in a Row in case they |
538 | + include joins.) |
539 | + @param fieldname: The name of this "field". The name should not |
540 | + contain a "." as that would make it look like |
541 | + <tablename>.<fieldname>, which would fool SQLTABLE3. |
542 | + @param label: Heading / label for use in lists / forms. |
543 | + @param method: A function to compute the desired value. It will be |
544 | + called as a method of the S3VirtualField instance, so its first |
545 | + argument should be the instance (usually "self" but if the |
546 | + function is defined inside a class, a different name should be |
547 | + used). In addition it should take a Row. It should return a |
548 | + value compatible with str(), for display in a list or form. It |
549 | + can use any internal helpers or data from S3VirtualField, via |
550 | + its instance argument, such as _get_field for extracting values |
551 | + from the Row. |
552 | + @param required_fields: Fields needed from the associated table. |
553 | + (This avoids the need for a query just to get other fields in |
554 | + the same table.) |
555 | + @param attr: Anything else we might want. |
556 | + Do not name these the same as any other parameters. |
557 | + """ |
558 | + |
559 | + self.db = db |
560 | + # @ToDo: If we want this to subclass Field, allow the user to set |
561 | + # the type (and default to, e.g., "string"). Then use isinstance, not |
562 | + # type, to check if this is an S3VirtualField. |
563 | + self.type = "s3_virtual_field" |
564 | + self.tablename = tablename |
565 | + self.name = fieldname |
566 | + if label: |
567 | + self.label = label |
568 | + else: |
569 | + self.label = fieldname |
570 | + self._represent = method |
571 | + self.required_fields = required_fields |
572 | + if attr: |
573 | + for key, value in attr.iteritems(): |
574 | + self.__dict__[key] = value |
575 | + self.writable = False |
576 | + |
577 | + # @ToDo: If we put something like this out where we can all get it, we |
578 | + # could share it... This one is very slightly pitched toward fake virtual |
579 | + # fields, which put their values in Row._extra, but is intended to work |
580 | + # for normal Row instances. |
581 | + def _get_field(self, row, colname): |
582 | + """ |
583 | + Get requested field's value from the row. |
584 | + |
585 | + If colname id unqualified it's assumed to be a fieldname from the |
586 | + table to which this virtual field belongs. |
587 | + """ |
588 | + |
589 | + if table_field.match(colname): |
590 | + (tablename, fieldname) = colname.split(".") |
591 | + else: |
592 | + fieldname = colname |
593 | + tablename = self.tablename |
594 | + if tablename in row: |
595 | + return row[tablename].get(fieldname, None) |
596 | + # Row's __getitem__ allows row["x"] and row.x if "x" is in row._extra, |
597 | + # but does not find "x" via either "x" in row or row.get("x",...). |
598 | + if "_extra" in row: |
599 | + if fieldname in row._extra: |
600 | + return row._extra[fieldname] |
601 | + elif colname in row._extra: |
602 | + return row._extra[colname] |
603 | + return None |
604 | + |
605 | + # @ToDo: Want this to be just __call__? |
606 | + def represent(self, value): |
607 | + """ |
608 | + Fetch a row if necessary, and call the supplied method. |
609 | + |
610 | + @param value: either a Row containing the required fields, or an id, |
611 | + in which case the row will be fetched. |
612 | + """ |
613 | + |
614 | + if isinstance(value, Row): |
615 | + row = value |
616 | + else: |
617 | + db = self.db |
618 | + query = db[self.tablename].id == value |
619 | + row = db(query).select(*self.required_fields, |
620 | + limitby=(0, 1)).first() |
621 | + if row: |
622 | + return str(self._represent(row)) |
623 | + else: |
624 | + return "" |
625 | + |
626 | + @staticmethod |
627 | + def add_virtual_values_to_row(row, *virtual_fields): |
628 | + """ |
629 | + Add values from the specified virtual fields to the row. |
630 | + |
631 | + @param row: a Row containing the required fields for the specified |
632 | + S3VirtualFields. |
633 | + @param virtual_fields: a list of S3VirtualField instances whose |
634 | + values to insert. |
635 | + |
636 | + It is the caller's responsibility to fix up colnames in the Rows, |
637 | + if needed. |
638 | + """ |
639 | + |
640 | + if not "_extra" in row: |
641 | + row["_extra"] = Row() |
642 | + |
643 | + for v in virtual_fields: |
644 | + row["_extra"][v.name] = v.represent(row) |
645 | + |
646 | + @staticmethod |
647 | + def add_virtual_values_to_rows(rows, *virtual_fields): |
648 | + """ |
649 | + Add values from the specified virtual fields to each row in rows. |
650 | + |
651 | + This does not add the colnames for the virtual fields to rows -- |
652 | + the caller should add whichever fields they want displayed. |
653 | + |
654 | + @param rows: a Rows where each Row contains the required fields for |
655 | + the specified S3VirtualFields. |
656 | + """ |
657 | + |
658 | + if not rows: |
659 | + return |
660 | + |
661 | + for row in rows: |
662 | + S3VirtualField.add_virtual_values_to_row(row, *virtual_fields) |
663 | + |
664 | +# Example of a subclass that can be reused for various tables. |
665 | + |
666 | +class S3VirtualFieldPersonContact(S3VirtualField): |
667 | + |
668 | + def _get_person_contact(self, row): |
669 | + db = self.db |
670 | + pid = self._get_field(row, "hrm_human_resource.person_id") |
671 | + query = (db.pr_person.id == pid) & \ |
672 | + (db.pr_person.pe_id == db.pr_contact.pe_id) & \ |
673 | + (db.pr_contact.contact_method == self.contact_method) |
674 | + result = db(query).select(db.pr_contact.value, limitby=(0,1)).first() |
675 | + if result: |
676 | + return result.value |
677 | + else: |
678 | + return "" |
679 | + |
680 | + def __init__(self, db, tablename, fieldname, label, contact_method, **attr): |
681 | + """ |
682 | + @param db: A DAL instance. |
683 | + @param tablename: The name of the table with which this "field" is |
684 | + associated. (Needed to dereference values in a Row in case they |
685 | + include joins.) |
686 | + @param fieldname: The name of this "field". |
687 | + @param label: Heading / label for use in lists / forms. (This is |
688 | + probably the value from pr_contact_method_opts corresponding to |
689 | + the index supplied in the following arg, but we don't have access |
690 | + to the opts here). |
691 | + @param contact_method: The index of the desired contact type from |
692 | + pr_contact_method_opts. |
693 | + @param attr: Anything else we might want. |
694 | + |
695 | + If the field name for the reference to pr_person is not person_id, |
696 | + it can be supplied as a keyword arg: |
697 | + @param person_id_fieldname: The foreign key reference to pr_person, |
698 | + if it's not person_id |
699 | + """ |
700 | + |
701 | + if "person_id_fieldname" in attr: |
702 | + self.person_id_fieldname = attr[person_id_fieldname] |
703 | + else: |
704 | + self.person_id_fieldname = "person_id" |
705 | + self.contact_method = contact_method |
706 | + S3VirtualField.__init__(self, db, tablename, fieldname, label, |
707 | + self._get_person_contact, |
708 | + [self.person_id_fieldname], |
709 | + **attr) |
Got questions? This doesn't really impact things that don't use the fake virtual fields, with one exception that's easy to fix, which is exporter.xls, to which support has not been added. If that is filtering fields that aren't in the table, then there's no effect. Otherwise, the quick way to avoid any interaction is to use report_fields for exporter.xls, and don't put any virtual fields in report_fields.
Here's one example use of a fake virtual field -- given population centers data, compute the population within a specified radius of some location. E.g. let the user pick the radius and set it in their session, then alter the gis_location list_fields to include that virtual field for a population centers query. This is (part of) a problem on the RHoK #3 list, proposed by Cat Graham, to go along with the previous population centers work.