Merge lp:~ptressel/sahana-eden/devel into lp:sahana-eden

Proposed by Pat Tressel
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
Reviewer Review Type Date Requested Status
nursix (community) Disapprove
Fran Boon Pending
Review via email: mp+63361@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Pat Tressel (ptressel) wrote :

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.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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_fields=["id",
                 S3ComputedValue(T("Email"), ...),
                 S3ComputedValue(T("Mobile Phone"), ...),
                 "type",
                 "job_title",
                 "status"]

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.

review: Needs Fixing
Revision history for this message
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

Revision history for this message
nursix (nursix.org) :
review: Abstain
Revision history for this message
nursix (nursix.org) wrote :

If necessary, re-implement this in a more recent version of CRUD.

Basically that would mean to just extend S3CRUD.get_list_fields, whereas the extra field pre-loading is already implemented. The S3VirtualField class doesn't seem to be necessary anymore.

review: Disapprove
Revision history for this message
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.get_list_fields, whereas
> the extra field pre-loading is already implemented. The S3VirtualField class
> doesn't seem to be necessary anymore.
>
> --
> https://code.launchpad.net/~ptressel/sahana-eden/devel/+merge/63361<https://code.launchpad.net/%7Eptressel/sahana-eden/devel/+merge/63361>
> 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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)