Programmatically Build Table in View

2,087 views
Skip to first unread message

Ross Peoples

unread,
Mar 3, 2011, 10:31:18 AM3/3/11
to web...@googlegroups.com
I have begun to move from models, modules, and controllers into actually writing the view for my application. Now, I am at the point where I need to loop through a Rows object, building a TR for each row. I wrote a version that works by writing it in HTML, using {{=x}} where needed, but it looks all cluttered and difficult to read. This is the original code that works:

{{extend 'layout.html'}}
<h1>Time Clock for {{=user.first_name}} {{=user.last_name}}</h1>
{{if len(rows) > 0:}}
    {{
        def get_day_of_year(the_date):
            return the_date.timetuple().tm_yday
            
        def full_date_name(the_date):
            return the_date.strftime('%A, %B %d, %Y')
            
        def time_string(the_date):
            return the_date.strftime('%I:%M %p')
            
        prev_day_of_year = None
        table_open = False
        for row in rows:
            if get_day_of_year(row.calculated_start) != prev_day_of_year:}}
                {{if table_open:}}
                    </tbody></table>
                {{pass}}
                <h4>{{=full_date_name(row.calculated_start)}}</h4>
                <table><thead><tr><th>Time in:</th><th>Time out:</th></tr></thead><tbody>
                <tr><td>{{=time_string(row.calculated_start)}}</td><td>{{=time_string(row.calculated_end)}}</td></tr>
                {{table_open = True}}
                {{prev_day_of_year = get_day_of_year(row.calculated_start)}}
            {{else:}}
                <tr><td>{{=time_string(row.calculated_start)}}</td><td>{{=time_string(row.calculated_end)}}</td></tr>
            {{pass}}
        {{pass}}
        </tbody></table>
    <h5>Total hours worked this week: {{=hours_worked}}</h5>
{{else:}}
    <h5>No time clock entries for this week.</h5>
{{pass}}

I know that web2py has HTML helpers like TABLE(), TR(), etc, but how would I build a table in this way, while in a for loop? This is what I tried so far, but it fails so horribly I don't even know where the error is:

{{extend 'layout.html'}}
<h1>Time Clock for {{=user.first_name}} {{=user.last_name}}</h1>
{{if len(rows) > 0:}}
    {{
        def get_day_of_year(the_date):
            return the_date.timetuple().tm_yday
            
        def full_date_name(the_date):
            return the_date.strftime('%A, %B %d, %Y')
            
        def time_string(the_date):
            return the_date.strftime('%I:%M %p')
            
        prev_day_of_year = None
        tables = []
        trs = None
        for row in rows:
            if get_day_of_year(row.calculated_start) != prev_day_of_year:
                if trs is not None:
                    tables[] = (H4(full_date_name(row.calculated_start)), trs)
                    trs = []
                    
                trs[] = TR((TD(time_string(row.calculated_start)), TD(time_string(row.calculated_end))))
                prev_day_of_year = get_day_of_year(row.calculated_start)
            else:
                trs[] = TR((TD(time_string(row.calculated_start)), TD(time_string(row.calculated_end))))
                
        for table in tables:
            response.write(table[0])
            response.write(TABLE(
                (THEAD(
                    TR(
                        (TH('Time in:), TH('Time out'))
                    )
                ),
                TBODY(
                    
                ))
            ))
    }}
    <h5>Total hours worked this week: {{=hours_worked}}</h5>
{{else:}}
    <h5>No time clock entries for this week.</h5>
{{pass}}

pbreit

unread,
Mar 3, 2011, 11:36:14 AM3/3/11
to web...@googlegroups.com
I usually try to put less logic in my views. For one, I try to assemble all the data fields ahead of time either in the controller or with virtual fields.

Then in the view, I try to just end up looping through the dict. I normally use HTML tags but using Web2py tag helpers should be fine as well.

See example below. Notice that I have a sliver of logic in there to display "no results" if there are no items. Also note that I have a couple fields like "item.last_date" that I have generated as a virtual field. In this way, you keep your views looking mostly like regular pages of HTML with some logic here and there.

    <table width="100%">
     <thead>
     <tr>
     <th></th>
     <th class="ttl">Description</th>
     <th class="prc">Current Price</th>
     <th class="prc">Final Price</th>
     </tr>
     </thead>
        <tbody>
        {{if len(items)==0:}}
            <tr><td colspan=4 align=middle><br>No items</td></tr>
        {{else:}}
            {{for item in items:}}
            <tr>
             <td>
             {{if len(item.image)==0:}}
                    {{=A(IMG(_src=URL('static','images/no-photo.png'), _width='80px'), _href=URL(c='default', f='item', extension='', args=item.id, ))}}
                {{else:}}   
                    {{=A(IMG(_src=URL(c='default', f='download', args=item.image), _width='100px'), _href=URL(c='default', f='item', extension='', args=item.id))}} 
                {{pass}}
                </td>
             <td class="ttl">{{=A(item.title, _href=URL(c='default', f='item', extension='', args=item.id))}}</td>
             <td class="prc"><b>${{=item.current_price}}</b></td>
             <td class="prc">${{=item.last_price}} on {{=item.last_date}}</td>

Ross Peoples

unread,
Mar 3, 2011, 11:44:41 AM3/3/11
to web...@googlegroups.com
That was a very helpful example. Thank you. So using the web2py TABLE helpers is best used for creating simple, static tables where the rows aren't generated? I guess that's ok, I always thought that the controller was meant to get the data, while the views format the data. But if the web2py way is to format in the controller and make the views contain as little Python code as possible, then that's they way I'll write it.

danto

unread,
Mar 3, 2011, 11:51:22 AM3/3/11
to web...@googlegroups.com


2011/3/3 Ross Peoples <ross.p...@gmail.com>
(...)But if the web2py way is to format in the controller and make the views contain as little Python code as possible, then that's they way I'll write it.

is not web2py's way, is MVC (Model-View-Controller) paradigm. it's all about separating logic from what end users view considerating some abstraction layers (model > controller > view). wikipedia could bring a some info too.

regards

pbreit

unread,
Mar 3, 2011, 11:59:50 AM3/3/11
to web...@googlegroups.com
Well, I wouldn't necessarily say that's the "web2py way". I just like to keep the logic in my views minimal. For example, the virtual fields are actually in the models which is also a good place to prepare your data.

One reason to limit logic in views is if you ever have graphic designers working on your views which is common in larger projects.

Some people like to have their code generate all of the HTML. I tend to prefer making it look like HTML with limited substitutions via code.

Anthony

unread,
Mar 3, 2011, 12:34:02 PM3/3/11
to web...@googlegroups.com
On Thursday, March 3, 2011 11:44:41 AM UTC-5, Ross Peoples wrote:
That was a very helpful example. Thank you. So using the web2py TABLE helpers is best used for creating simple, static tables where the rows aren't generated?
 
I think you should be able to build a table programmatically using helpers. See the "TABLE, TR, TD" section here: http://www.web2py.com/book/default/chapter/05#Built-in-Helpers (shows an example using a list comprehension).
 
I guess that's ok, I always thought that the controller was meant to get the data, while the views format the data.
 
That's probably the right way to think about it (from an MVC perspective), though I suppose what it means for the view to get "the data" can be a judgment call. You have to decide how you want to structure the data before passing it to the view for presentation. In your case, it looks like you've got a single set of rows, which you are then breaking into multiple tables -- so you have to ask yourself whether it makes sense for the controller (business logic) to decide how to break up the rows into separate tables, or whether that's purely a presentation issue. If the former, then maybe create a data structure that breaks up the rows in the controller, and have the view simply build the table for each set of rows returned by the controller. You might also think about what you would want the controller to return in case its output was going to a different view (e.g., JSON) -- if you need to structure the data differently depending on the view, then it might make sense to do the structuring in the view rather than the controller.
 
Anthony

pbreit

unread,
Mar 3, 2011, 2:04:33 PM3/3/11
to web...@googlegroups.com
And furthering my example, in that very same view I have another table that displays the same set of items in grid format. Note I included a counter "i" and a "modulo 4" to insert a </tr><tr> to break it up into rows of 4 columns. Also note the logic that inserts a placeholder image if an image is not present.

So it's not that there is no logic in the view, it's just keeping it sort of minimal. Also, I expect to move the styles into css files at some point.


    <table>
        <tbody>
        {{if len(items)==0:}}
            <tr><td colspan=4 align=middle><br>No items</td></tr>
        {{else:}}
            <tr>
                {{i=1}}
                {{for item in items:}}
                 <td width="180">
                    <p>{{if len(item.image)==0:}}
                            {{=A(IMG(_src=URL('static','images/no-photo.png'), _width='160px'), _href=URL(c='default', f='item', extension='', args=item.id, ))}}
                        {{else:}}   
                            {{=A(IMG(_src=URL(c='default', f='download', args=item.image), _width='160px'), _href=URL(c='default', f='item', extension='', args=item.id))}} 
                        {{pass}}</p>
                    <p style="min-height: 34px;">{{=A(item.title, _href=URL(c='default', f='item', extension='', args=item.id))}}</p>
                    <p><table width="100%"><tr><td style="padding: 2px 0px 2px 2px;"><b>${{=item.current_price}}</b><br></td>
                    <td align=right style="padding: 2px 8px 2px 2px;">${{=item.last_price}} on {{=item.last_date}}</td></tr></table>
                 </td></p>
                    {{if i%4==0:}}
                        </tr>
                        <tr>
                    {{pass}}
                    {{i+=1}}
                {{pass}}
            </tr>
        {{pass}}
        </tbody>
    </table>

pbreit

unread,
Mar 3, 2011, 2:06:08 PM3/3/11
to web...@googlegroups.com
I probably could modulo the size of dict instead of using a counter but I didn't take the time to figure that out. Any assistance on that?

Joe Barnhart

unread,
Mar 3, 2011, 2:44:08 PM3/3/11
to web2py-users
One thing I find handy about the helper objects is that they can be
treated as lists:

{{t=TABLE()}}
...
{{t.append(TR(val11,val12,val13))
...
{{t.append(TR(val21,val22,val23))
...
{{=t}} <-- this is when the table is added to the response

Note that you also do not need to use TD objects specifically (unless
you want to add class, etc.) TR will take multiple args and wrap each
with a TD automatically.

-- Joe

Ross Peoples

unread,
Mar 3, 2011, 2:45:06 PM3/3/11
to web...@googlegroups.com
Thanks Joe, I was wondering about this.

Anthony

unread,
Mar 3, 2011, 4:07:00 PM3/3/11
to web...@googlegroups.com
On Thursday, March 3, 2011 10:31:18 AM UTC-5, Ross Peoples wrote:
I know that web2py has HTML helpers like TABLE(), TR(), etc, but how would I build a table in this way, while in a for loop? This is what I tried so far, but it fails so horribly I don't even know where the error is:

{{extend 'layout.html'}}
<h1>Time Clock for {{=user.first_name}} {{=user.last_name}}</h1>
{{if len(rows) > 0:}}
    {{
        def get_day_of_year(the_date):
            return the_date.timetuple().tm_yday
            
        def full_date_name(the_date):
            return the_date.strftime('%A, %B %d, %Y')
            
        def time_string(the_date):
            return the_date.strftime('%I:%M %p')
            
        prev_day_of_year = None
        tables = []
        trs = None
        for row in rows:
            if get_day_of_year(row.calculated_start) != prev_day_of_year:
                if trs is not None:
                    tables[] = (H4(full_date_name(row.calculated_start)), trs)
                    trs = []
                    
                trs[] = TR((TD(time_string(row.calculated_start)), TD(time_string(row.calculated_end))))
                prev_day_of_year = get_day_of_year(row.calculated_start)
            else:
                trs[] = TR((TD(time_string(row.calculated_start)), TD(time_string(row.calculated_end))))
 
Am I missing something here? Where are you building a table -- I don't see any calls to TABLE(), just TR(). Also, I'm not sure about your syntax. It looks like you are passing a tuple of TD's to TR instead of passing the TD's as separate arguments (i.e., there's an extra set of parentheses inside TR). Finally, what is 'trs[] =' doing -- I think that will raise a syntax error. You should be able to create a table object (e.g., table = TABLE()), and then append TR's to it via append -- e.g., table.append(TR('cell 1', 'cell 2')).
 
Anthony
 

Ross Peoples

unread,
Mar 3, 2011, 4:14:34 PM3/3/11
to web...@googlegroups.com
After seeing all the other posts, I completely rewrote the whole thing. I grouped the data in the controller so that entries on the same day will be in the same list, making easier to put each day of entries into its own table. I have really advanced the whole thing since my original post. Here is my controller:

def format_minutes(minutes):
        hours = round(minutes / 60)
        if hours == 0:
            return "00:%02d" % (minutes)
        else:
            minutes -= minutes * hours
            return "%02d:%02d" % (hours, minutes)
        
    def format_full_date(the_date):
        return the_date.strftime('%A, %B %d, %Y')
        
    # get information from database
    week_start, week_end = timeclock.get_week_range_for_date()
    rows = timeclock.list_for_user(auth.user, week_start, week_end)
    minutes_worked = timeclock.get_minutes_for_this_week(auth.user)
    minutes_vacation = timeclock.get_minutes_for_this_week(auth.user, 2)
    minutes_personal = timeclock.get_minutes_for_this_week(auth.user, 3)
    minutes_sick = timeclock.get_minutes_for_this_week(auth.user, 4)
    minutes_total = minutes_worked + minutes_vacation + minutes_personal + minutes_sick
    
    # begin formatting minutes for the week
    hours_worked = format_minutes(minutes_worked)
    hours_vacation = format_minutes(minutes_vacation)
    hours_personal = format_minutes(minutes_personal)
    hours_sick = format_minutes(minutes_sick)
    hours_total = format_minutes(minutes_total)
    
    # group entries together to make it easier for the view
    grouped_entries = dict()
    for row in rows:
        full_date = format_full_date(row.calculated_start)
        if full_date in grouped_entries:
            grouped_entries[full_date].append(row)
        else:
            grouped_entries[full_date] = [row]
    
    return dict(grouped_entries=grouped_entries,
        hours_worked=hours_worked, hours_vacation=hours_vacation,
        hours_personal=hours_personal, hours_sick=hours_sick, hours_total=hours_total
    )


And this is my view:

{{extend 'layout.html'}}
{{import datetime}}
<h1>Time Clock for {{=auth.user.first_name}} {{=auth.user.last_name}}</h1>

{{still_clocked_in = False}}
<div id="timeclock_list">
{{if len(grouped_entries) > 0:}}
    {{
        def format_time_string(the_date):
            if the_date is None: return 'Still clocked in'
            return the_date.strftime('%I:%M %p')
            
        def format_minutes(minutes):
            hours = round(minutes / 60)
            if hours == 0:
                return "00:%02d" % (minutes)
            else:
                minutes -= minutes * hours
                return "%02d:%02d" % (hours, minutes)
            pass
    }}
    
    {{for k, v in grouped_entries.iteritems():}}
        <h4>{{=k}}</h4>
        <table><thead><tr><th>Time in:</th><th>Time out:</th><th>Type:</th><th>Hours:</th></tr></thead><tbody>
        
        {{total_for_day = 0}}
        
        {{for entry in v:}}
            {{
                if entry.calculated_minutes is None:
                   still_clocked_in = True
                   entry_hours = ''
                else:
                    total_for_day += entry.calculated_minutes
                    entry_hours = format_minutes(entry.calculated_minutes)
                pass
                
                date_start = format_time_string(entry.calculated_start)
                date_end = format_time_string(entry.calculated_end)
                entry_type = db.timeclock_events[entry.event_type]
            }}
            <tr><td>{{=date_start}}</td><td>{{=date_end}}</td><td>{{=entry_type}}</td><td class="center">{{=entry_hours}}</td></tr>
        {{pass}}
        
        {{total_for_day_formatted = format_minutes(total_for_day)}}
        </tbody><tfoot><tr><th class="right" colspan="3">Total for day:</th><th>{{=total_for_day_formatted}}</th></tr></tfoot></table>
    {{pass}}
</div>

<div id="timeclock_totals">
    <h4>Totals for week</h4>
    <table><tbody>
    
    {{if hours_worked != '00:00':}}
        <tr><td>Total hours worked this week:</td><td class="center">{{=hours_worked}}</td></tr>
    {{pass}}
    
    {{if hours_vacation != '00:00':}}
        <tr><td>Total hours of vacation this week:</td><td class="center">{{=hours_vacation}}</td></tr>
    {{pass}}
    
    {{if hours_personal != '00:00':}}
        <tr><td>Total hours of personal time this week:</td><td class="center">{{=hours_personal}}</td></tr>
    {{pass}}
    
    {{if hours_sick != '00:00':}}
        <tr><td>Total hours sick this week:</td><td class="center">{{=hours_sick}}</td></tr>
    {{pass}}
    
    </tbody><tfoot>
    <tr><th class="right">Grand total for this week:</th><th>{{=hours_total}}</th></tr>
    </tfoot></table>
{{else:}}
    <h5>No time clock entries for this week.</h5>
{{pass}}
</div>

{{
    if still_clocked_in:
        punch_text = 'Clock out'
    else:
        punch_text = 'Clock in'
    pass
    
    response.write(A(punch_text, _href=URL('punch'), _id='timeclock_punch', _class='no_print'))
}}

There is a lot more code because I have since added a lot functionality, but I think the code itself is fairly clean. I put most of the computational stuff in the controller, and only a couple of things needed to be formatted for the view.

pbreit

unread,
Mar 3, 2011, 4:19:06 PM3/3/11
to web...@googlegroups.com
Yeah, I got kind of lost, too. The thing that threw me off were "trs[] =" and "tables[] =". The = sign is for assignment, not adding or appending. Each "trs[] =" is re-assigning the whole variable each time, overwriting anything that was already there (if I'm not mistaken).

But building tables with functions is more difficult for me in general. That may be because I learned HTML tables first.

Ross Peoples

unread,
Mar 3, 2011, 4:21:40 PM3/3/11
to web...@googlegroups.com
Yea, for some reason I thought that 'tables[] = ' would append to the list. I'm not sure which language that's from, but it doesn't matter now, as it is certainly much more readable to me. Before, the view had a hacky way of grouping the data into a table and I was confused about the meaning of the code seconds after I wrote it, so I figured a cleaner rewrite was in order :)

Anthony

unread,
Mar 3, 2011, 4:26:19 PM3/3/11
to web...@googlegroups.com
On Thursday, March 3, 2011 4:19:06 PM UTC-5, pbreit wrote:
Yeah, I got kind of lost, too. The thing that threw me off were "trs[] =" and "tables[] =". The = sign is for assignment, not adding or appending. Each "trs[] =" is re-assigning the whole variable each time, overwriting anything that was already there (if I'm not mistaken).
 
Actually, I think that simply raises a syntax error.

Anthony

unread,
Mar 3, 2011, 4:30:16 PM3/3/11
to web...@googlegroups.com
I think
 
trs[:] = something
 
would overwrite trs, as long as 'something' is an iterable.
 

mart

unread,
Mar 3, 2011, 5:21:47 PM3/3/11
to web2py-users
hey,


here is what I do with plugin_wiki (usually for automated reports, but
here is a simplified sample):


header = ['First_Name', 'Last_Name', 'Age', 'Description']
data = \
'''fred,Flintstone,32,always gets into trouble,
Barney,Rubble,33,is always willing to help Fred
Mr,Slate,31,good businees man but not a people person
Dino,dinosaur,3,looks like he should be a dog
Pebbles,Flintstone,2,Great kid but does not talk much'''


if isinstance(data,str):
rows = [row.strip().split(',') for row in data.splitlines()]
if header is not None:
nHeader = []
for h in header:
nHeader.append('**{0}**'.format(h))
markminTable = table_lists2Markmin([nHeader] + rows)
else:
markminTable = table_lists2Markmin([header] + rows)
print markminTable


# here is the function that makes a markmin table (sorry if the
formatting looks bad here):

def table_lists2Markmin(rows):
tableRows = deque()
for tRows in [([[sStr for sStr in item] for item in map(None,
*[item.split('\n') for item in row])]) for
row in rows]:
for tableRow in tRows:
tableRows.append('{0}'.format(' |
'.join([string.join(string.strip(item),
'')
for item in tableRow])))
return '-------\n{0}\n-------'.format(string.join(tableRows,'\n'))


Usually the string "data" (it can be a list or a table as well) gets
passed to a function that will make use (not shown here be it has too
many lines) of anything we have in markmin by doing something like
below.

data = \
'''fred,Flintstone,32,always gets into trouble,
Barney,Rubble,33,is always willing to help Fred
Mr,Slate,31,good business man and this is his {0}
Dino,dinosaur,3,looks like he should be a dog
Pebbles,Flintstone,2,Great kid but does not talk much
'''.format(mmObj.mLink('favorite link','http:/www.google.com'))


makes automating Markmin tables easy through automation and decorating
the fileds as well.

hope it helps,
Mart :)
Reply all
Reply to author
Forward
0 new messages