Changeset 5302 for MILK


Ignore:
Timestamp:
18/05/09 15:32:54 (10 years ago)
Author:
cbyrom
Message:

Simplify structure of discovery controller, using the results template
to render searches even when no results are found. Add new error
dict to avoid confusing with the error dict used by the formencode
input checker. Add additional input checks and improve error handling.
Add dropdown list to select order by values + add new constants
module to store associated data and error keys + tidy up DiscoveryState?
object to make clearer and logic more consistent + fix paging controls
for results data.

Location:
MILK/trunk/milk_server/milk_server
Files:
1 added
9 edited

Legend:

Unmodified
Added
Removed
  • MILK/trunk/milk_server/milk_server/config/milkMiddleware.py

    r5299 r5302  
    77from ndg.common.src.clients.http.vocabserverclient import VocabServerClient 
    88import milk_server.lib.helpers as h 
    9 import milk_server.lib.constants as constants 
    109 
    1110class NDGConfigError(Exception):   
     
    9796        self.globals.eXistDBCons = {} 
    9897         
    99         self.globals.orderByList = h.options_for_select(constants.ORDER_BY_LIST) 
    100  
    10198        # Security Related 
    10299 
  • MILK/trunk/milk_server/milk_server/controllers/browse/browserconstants.py

    r5261 r5302  
    1212INVALID_BBOX_MESSAGE = "Invalid bounding box dimensions entered: " 
    1313INVALID_DATERANGE_MESSAGE = "Invalid dates entered: " 
     14INCOMPLETE_DATERANGE_MESSAGE = "Incomplete date range entered: " 
  • MILK/trunk/milk_server/milk_server/controllers/browse/discovery.py

    r5261 r5302  
    1313from milk_server.lib.base import * 
    1414from milk_server.lib.Date import * 
    15 from milk_server.models.DiscoveryState import DiscoveryState,constraints 
     15from milk_server.models.DiscoveryState import DiscoveryState, constraints 
    1616from milk_server.controllers.home import HomeController 
    1717import browserconstants as bc 
     18from milk_server.lib.Utilities import getURLConstraints 
     19import milk_server.lib.constants as constants 
     20 
    1821 
    1922class DiscoveryController(HomeController): 
     
    2831        self.cf=request.environ['ndgConfig'] 
    2932        self.inputs=dict(parse_querystring(request.environ)) 
     33         
    3034        self.message='' 
    31         c.errors = {}    # dict to store error messages 
    32                 
     35        c.inputErrors = {}    # dict to store error messages 
     36         
     37        c.discoveryUrl = h.url_for('discovery') 
     38 
     39        orderBySelect = "" 
     40        if 'orderBy' in self.inputs: 
     41            orderBySelect = self.inputs['orderBy'] 
     42        c.orderByList = h.options_for_select(constants.ORDER_BY_LIST, orderBySelect)                
    3343     
    3444    def index(self): 
     
    4959        
    5060        self.__getInputs() 
    51  
     61         
    5262        # if any errors are found, return user to search page 
    53         if c.errors: 
     63        if c.inputErrors: 
    5464            return self.__advancedPrompt() 
    5565 
     
    8595        ''' 
    8696        logging.debug("Getting user inputs") 
     97         
     98        # restore contraints from input, if set 
     99        if 'constraints' in self.inputs: 
     100            constraints = getURLConstraints(self.inputs['constraints']) 
     101            del self.inputs['constraints'] 
     102            self.inputs.update(constraints) 
     103                     
     104        if 'vocabTerm' in self.inputs and 'searchString' not in self.inputs: 
     105            self.inputs['searchString'] = "" 
     106             
    87107        # see if this is a discovery search or a more complicated search 
    88108        if 'searchTarget' not in self.inputs:  
     
    113133        missingInputs = self.__checkform(expected) 
    114134        if missingInputs: 
    115             if bc.INCOMPLETE_SEARCH_INPUT_MESSAGE not in c.errors: 
    116                 c.errors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE] = [] 
    117             c.errors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE].extend(missingInputs) 
     135            if bc.INCOMPLETE_SEARCH_INPUT_MESSAGE not in c.inputErrors: 
     136                c.inputErrors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE] = [] 
     137            c.inputErrors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE].extend(missingInputs) 
    118138 
    119139        self.__getSpatioTemporalInputs() 
     140         
    120141        logging.debug("User inputs retrieved") 
    121142 
     
    152173            errors = self.__checkBBoxValidity(self.bbox) 
    153174            if errors: 
    154                 if bc.INVALID_BBOX_MESSAGE not in c.errors: 
    155                     c.errors[bc.INVALID_BBOX_MESSAGE] = [] 
    156                 c.errors[bc.INVALID_BBOX_MESSAGE].extend(missingInputs) 
    157  
     175                if bc.INVALID_BBOX_MESSAGE not in c.inputErrors: 
     176                    c.inputErrors[bc.INVALID_BBOX_MESSAGE] = [] 
     177                c.inputErrors[bc.INVALID_BBOX_MESSAGE].extend(errors) 
     178 
     179        # NB 
    158180        missingInputs = self.__checkform(['startDate', 'endDate']) 
    159         if missingInputs or not(self.inputs['startDate'] and self.inputs['endDate']): 
     181        if missingInputs: 
    160182            self.dateRange = None 
     183        elif self.inputs['startDate'] and not self.inputs['endDate']: 
     184            c.inputErrors[bc.INCOMPLETE_DATERANGE_MESSAGE] = ['End date missing'] 
     185        elif not self.inputs['startDate'] and self.inputs['endDate']: 
     186            c.inputErrors[bc.INCOMPLETE_DATERANGE_MESSAGE] = ['Start date missing'] 
    161187        else: 
    162188            dateError = None 
     
    177203 
    178204            if dateError: 
    179                 if bc.INVALID_DATERANGE_MESSAGE not in c.errors: 
    180                     c.errors[bc.INVALID_DATERANGE_MESSAGE] = [] 
    181                 c.errors[bc.INVALID_DATERANGE_MESSAGE].append(dateError) 
     205                if bc.INVALID_DATERANGE_MESSAGE not in c.inputErrors: 
     206                    c.inputErrors[bc.INVALID_DATERANGE_MESSAGE] = [] 
     207                c.inputErrors[bc.INVALID_DATERANGE_MESSAGE].append(dateError) 
    182208         
    183209        logging.debug("Spatiotemporal inputs retrieved") 
     
    291317         
    292318        hits = searchClient.hits 
     319        # NB, this is used in the semantic search function of results.kid and short_results.kid 
     320        c.querystring = request.environ['QUERY_STRING'] 
     321 
     322        difs = [] 
     323        errors = [] 
     324 
    293325        if hits == 0 and ds.constraintsInstance['textTarget'] != SearchClient.ATOM_TARGET: 
    294             outMessage = 'No records found [constraints: %s]' %ds.constraints 
     326            outMessage = 'No records found for "%s"[constraints: %s]' \ 
     327                %(ds.searchString, ds.constraints) 
    295328            logging.info(outMessage)  
    296329            c.xml='<p>' + outMessage + '</p>' 
    297             return render('content') 
    298          
    299         # NB, this is used in the semantic search function of results.kid and short_results.kid 
    300         c.querystring = request.environ['QUERY_STRING'] 
    301       
    302         try: 
    303             # display browse search results differently 
    304             if self.inputs['searchTarget'] != 'Discovery': 
    305                 return self.__displayBrowseSearchResults(searchClient) 
    306  
    307             # now actually retrieve the search records 
    308             results = searchClient.getLabelledDocs(format='DIF') 
    309  
    310             if not results: 
    311                 c.xml='<p> No results for "%s"!</p>'%ds.searchString 
    312                 return render('content') 
    313  
    314             difs = [] 
    315             errors = [] 
    316             for result in results:  
    317                 obj=ndgObject(result[0], config = self.cf) 
    318                 try: 
    319                     difs.append(DIF(result[1],ndgObj=obj)) 
    320                 except ValueError,e: 
    321                     errors.append((result[0], str(e))) 
    322  
    323             if not difs: 
    324                 c.xml='<p>No usable results for "%s"!</p>'%ds.searchString 
    325                 return render('content') 
    326              
    327             elif errors: 
    328                 c.xml='<p>Search results for "%s"'%ds.searchString 
    329                 dp=[] 
    330                 for e in errors: 
    331                     n=ndgObject(e[0]) 
    332                     if n.repository not in dp:  
    333                         dp.append(n.repository) 
    334                 if len(dp)<>1:  
    335                     dp='[Various Data Providers]' 
     330 
     331        else: 
     332            try: 
     333                # display browse search results differently 
     334                if self.inputs['searchTarget'] != 'Discovery': 
     335                    return self.__displayBrowseSearchResults(searchClient) 
     336     
     337                # now actually retrieve the search records 
     338                results = searchClient.getLabelledDocs(format='DIF') 
     339     
     340                if not results: 
     341                    c.xml='<p>No results for "%s"!</p>'%ds.searchString 
    336342                else: 
    337                     dp='[%s]'%dp[0]  
     343                    for result in results:  
     344                        obj=ndgObject(result[0], config = self.cf) 
     345                        try: 
     346                            difs.append(DIF(result[1],ndgObj=obj)) 
     347                        except ValueError,e: 
     348                            errors.append((result[0], str(e))) 
     349         
     350                    if not difs: 
     351                        c.xml='<p>No usable results for "%s"!</p>'%ds.searchString 
    338352                     
    339                 c.xml+=' (unfortunately %s hits matched unformattable documents from %s, an internal error has been logged):</p>'%(len(errors),dp) 
    340                 status, message=mailHandler([g.metadataMaintainer],'DIF errors', 
    341                                             str(errors), server = g.mailServer) 
    342                 if not status: 
    343                     c.xml+='<p> Actually, not even an internal error has been logged. <br/>' 
    344                     c.xml+='Internal sending of mail failed with error [%s]</p>'%message 
    345                 return render('content') 
     353                    elif errors: 
     354                        c.xml='<p>Search results for "%s"'%ds.searchString 
     355                        dp=[] 
     356                        for e in errors: 
     357                            n=ndgObject(e[0]) 
     358                            if n.repository not in dp:  
     359                                dp.append(n.repository) 
     360                        if len(dp)<>1:  
     361                            dp='[Various Data Providers]' 
     362                        else: 
     363                            dp='[%s]'%dp[0]  
     364                             
     365                        c.xml+=' (unfortunately %s hits matched unformattable documents from %s, an internal error has been logged):</p>'%(len(errors),dp) 
     366                        status, message=mailHandler([g.metadataMaintainer],'DIF errors', 
     367                                                    str(errors), server = g.mailServer) 
     368                        if not status: 
     369                            c.xml+='<p> Actually, not even an internal error has been logged. <br/>' 
     370                            c.xml+='Internal sending of mail failed with error [%s]</p>'%message 
     371                     
     372                # if we're here, we're ready to display the dif records 
     373                c.difs = difs 
     374                session['results'] = h.current_url() 
     375                session.save() 
    346376                 
    347             # if we're here, we're ready to display the dif records 
    348             c.difs = difs 
    349             session['results'] = h.current_url() 
    350             session.save() 
    351              
    352             # set up the displayed tabs 
    353             if len(c.pageTabs)==1:  
    354                 c.pageTabs.append(('Results', session['results'])) 
    355                 c.pageTabs.append(('Selections', 
    356                                    h.url_for(controller='browse/selectedItems', 
    357                                              action='index'))) 
    358             elif c.pageTabs[1][0]!='Results': 
    359                     c.pageTabs.insert(1,('Results',session['results'])) 
    360                     selectionsNeeded=1 
    361                     for tab in c.pageTabs[0]: 
    362                         if tab == 'Selections': 
    363                             selectionsNeeded=0 
    364                     if selectionsNeeded: 
    365                         c.pageTabs.append(('Selections', 
    366                                    h.url_for(controller='browse/selectedItems', 
    367                                              action='index'))) 
    368                          
    369             return render('browse/results') 
    370                  
    371         except ValueError,e: 
    372             if g.debugModeOn == 'True': 
    373                 raise ValueError,str(e) 
    374             else: 
     377                # set up the displayed tabs 
     378                if len(c.pageTabs)==1:  
     379                    c.pageTabs.append(('Results', session['results'])) 
     380                    c.pageTabs.append(('Selections', 
     381                                       h.url_for(controller='visualise/selectedItems', 
     382                                                 action='index'))) 
     383                elif c.pageTabs[1][0]!='Results': 
     384                        c.pageTabs.insert(1,('Results',session['results'])) 
     385                        selectionsNeeded=1 
     386                        for tab in c.pageTabs[0]: 
     387                            if tab == 'Selections': 
     388                                selectionsNeeded=0 
     389                        if selectionsNeeded: 
     390                            c.pageTabs.append(('Selections', 
     391                                       h.url_for(controller='visualise/selectedItems', 
     392                                                 action='index'))) 
     393                             
     394                     
     395            except ValueError,e: 
     396                if g.debugModeOn == 'True': 
     397                    raise ValueError,str(e) 
     398 
    375399                c.xml='<p> Error retrieving documents for %s hits is [%s]</p>'%(hits,e) 
    376                 return render('content') 
    377  
     400 
     401        return render('browse/results') 
    378402         
    379403    def __advancedPrompt(self, searchConstraints = None): 
  • MILK/trunk/milk_server/milk_server/controllers/visualise/viewItems.py

    r4487 r5302  
    1111""" 
    1212 
    13 from ows_server.lib.base import * 
     13from milk_server.lib.base import * 
    1414from paste.request import parse_querystring 
    15 from ows_server.models import Utilities 
    16 from ows_server.models import selectedItem 
     15from milk_server.lib import Utilities 
     16from milk_server.models import selectedItem 
    1717import logging 
    1818 
  • MILK/trunk/milk_server/milk_server/lib/Utilities.py

    r4959 r5302  
    7070    """ 
    7171    return [urllib.unquote(x) for x in string.split('|')] 
     72 
     73 
     74def getURLConstraints(urlConstraints): 
     75    ''' 
     76    Given a set of contraints passed as a uri parameter, convert these into 
     77    a dict and return this, avoiding duplicate entries 
     78    @return dict with key, val = param, value 
     79    ''' 
     80    constraints = {} 
     81    inputs = urlConstraints.split('&') 
     82    for input in inputs: 
     83        data = input.split('=') 
     84        if len(data) > 2: 
     85            import pdb 
     86            pdb.set_trace() 
     87        key, val = input.split('=') 
     88        if val: 
     89            constraints[key] = val 
     90    return constraints 
     91 
  • MILK/trunk/milk_server/milk_server/models/DiscoveryState.py

    r5261 r5302  
    11from paste.request import parse_querystring 
     2from milk_server.lib.Utilities import getURLConstraints 
    23import cgi,urllib 
     4 
    35class constraints: 
    46    '''  
     
    6163        self.constraintsInstance=constraints 
    6264        self.constraints=str(constraints) # some text to show constraints on search 
    63         self.constrainedurl=self.geturl(constrained=1)+'&constrained' 
     65        self.urlformattedconstraints = self.__getURLFormattedConstraints() 
    6466        self.sessID=sessionID 
    6567        self.hits=hits 
     
    6870        self.searchString=searchString 
    6971        self.alternatives=None 
     72        self.constrainedurl=self.geturl(constrained=1)+'&constrained' 
     73 
     74     
     75    def __getURLFormattedConstraints(self,**kw): 
     76        '''  
     77        Get the constraints in a url friendly string - modified by the keyword arguments  
     78        offset and stride which are to be part of the querystring  
     79        ''' 
     80        args = dict(parse_querystring(self.environ)) 
     81        offset, stride= kw.get('offset'), kw.get('stride') 
     82        if offset is not None: 
     83            args['start']=offset 
     84        if stride is not None: 
     85            args['howmany']=stride 
     86 
     87        constrained=kw.get('constrained') 
     88        if constrained is not None: 
     89            if 'start' in args:  
     90                del args['start'] 
     91            if 'howmany' in args:  
     92                del args['howmany'] 
     93 
     94        q = '' 
     95        for i in args: 
     96            # NB, the constraints may already be encoded as a hidden variable 
     97            if i != 'constraints': 
     98                q+='%s=%s&'%(i,args[i]) 
     99 
     100        # add constraints last to avoid duplicate params - NB, constraint params 
     101        # can be overridden by direct inputs to the page 
     102        if i == 'constraints': 
     103            constraints = getURLConstraints(args[i]) 
     104            for key, val in constraints.items(): 
     105                if key not in args: 
     106                    q+='%s=%s&'%(key, val) 
     107 
     108        return q[0:-1] 
    70109 
    71110     
    72111    def geturl(self,**kw): 
    73112        '''  
    74         Get a url from the wsgi environment, modified by the keyword arguments  
    75         offset and stride which are to be part of the querystring  
     113        Get a url from the wsgi environment 
    76114        ''' 
    77         args=dict(parse_querystring(self.environ)) 
    78         offset,stride=kw.get('offset'),kw.get('stride') 
    79         if offset is not None:args['start']=offset 
    80         if stride is not None:args['howmany']=stride 
    81         constrained=kw.get('constrained') 
    82         if constrained is not None: 
    83             if 'start' in args: del args['start'] 
    84             if 'howmany' in args: del args['howmany'] 
    85         q='?' 
    86         for i in args: q+='%s=%s&'%(i,args[i]) 
    87         q=q[0:-1] 
    88         url=urllib.quote(self.environ.get('SCRIPT_NAME','')) + \ 
    89             urllib.quote(self.environ.get('PATH_INFO','')) + q 
    90         #url=cgi.escape(url) 
    91         return url 
     115        constraints = self.__getURLFormattedConstraints(**kw) 
     116        return urllib.quote(self.environ.get('SCRIPT_NAME','')) + \ 
     117            urllib.quote(self.environ.get('PATH_INFO','')) + '?' + constraints 
    92118 
    93119     
    94120    def getNext(self): 
    95         ''' Get the next slice ''' 
     121        '''  
     122        Get info on what number of records are available on current page 
     123        @return result - list with format [[offSetForNextPage, numberOfRecordsOnNextPage], 
     124                                           [offSetForLastPage, numberOfRecordsOnLastPage]] 
     125        ''' 
    96126        result=[] 
    97127        defStride=10 
    98         if self.offset+self.stride<self.hits: 
     128        if self.offset+self.stride < self.hits: 
    99129            #there are more to look at 
    100             r=[self.offset+self.stride,self.stride] 
    101             if r[0]+r[1]-1>self.hits: r[1]=self.hits+1-r[0] 
     130            r = [self.offset+self.stride, self.stride] 
     131            if r[0]+r[1]-1>self.hits:  
     132                r[1] = self.hits+1-r[0] 
    102133            result.append(r) 
    103         else:result.append([]) 
    104         if self.offset>1: 
     134        else: 
     135            result.append([]) 
     136             
     137        if self.offset > 1: 
    105138            #there are previous records 
    106             b=max(self.stride,defStride) 
     139            b=max(self.stride, defStride) 
    107140            r=[self.offset-b,b] 
    108             if r[0]<1: r[0]=1 
    109             if r[1]>self.hits: r[1]=self.hits 
     141            if r[0]<1:  
     142                r[0]=1 
     143            if r[1]>self.hits:  
     144                r[1]=self.hits 
    110145            result.append(r) 
    111         else: result.append([]) 
     146        else:  
     147            result.append([]) 
    112148        return result 
    113149         
  • MILK/trunk/milk_server/milk_server/templates/browse/results.kid

    r4487 r5302  
    2020            <?python 
    2121            n,p=c.state.getNext() 
    22             if p!=[]:purl=c.state.geturl(offset=p[0],stride=p[1]) 
    23             if n!=[]:nurl=c.state.geturl(offset=n[0],stride=n[1]) 
     22            if p: 
     23                purl=c.state.geturl(offset=p[0],stride=p[1]) 
     24            if n: 
     25                nurl=c.state.geturl(offset=n[0],stride=n[1]) 
    2426            upper=c.state.offset+c.state.stride-1 
    2527            ?> 
    2628            <div class="resultsBar">  
    2729                <div> 
     30                    <form action="$c.discoveryUrl" name="orderByForm"> 
     31                                <input type="hidden" name="constraints" value="${c.state.urlformattedconstraints}"/> 
    2832                    <a href="${c.state.constrainedurl}"> Refine search</a><span py:replace="helpIcon('refser_help')"/> 
    29                     | Found ${c.state.hits} | Showing ${c.state.offset}-$upper  
    30                     <span py:if="p!=[]"> | <a href="$purl"> Previous ${p[1]}</a> </span> 
    31                     <span py:if="n!=[]"> | <a href="$nurl"> Next ${n[1]}</a></span> |  
     33                    <span py:if="c.state.hits != 0" py:strip="">  
     34                                | Order By ${XML(h.select('orderBy',option_tags = c.orderByList, onchange="orderByForm.submit()"))} 
     35                        | Found ${c.state.hits}  
     36                            <span py:if="p!=[]"> | <a href="$purl"> Previous ${p[1]}</a> </span> 
     37                                                | Showing ${c.state.offset}-$upper  
     38                        <span py:if="n!=[]"> | <a href="$nurl"> Next ${n[1]}</a></span> 
     39                     </span> 
     40                            </form> 
    3241                </div><div id="refser_help" class="hidden"> 
    3342                    <div class="helptxt"><p> 
     
    3645                </div> 
    3746            </div> 
    38             <div id="resultsTab"> 
     47            <div py:if="c.state.hits != 0" id="resultsTab"> 
    3948                <table> 
    4049                    <thead><tr><th rowspan="2">Dataset description</th><th colspan="2">Temporal&nbsp;coverage</th> 
     
    5160                </table> 
    5261            </div> 
    53             <div class="resultsBar"> 
     62            <div py:if="c.state.hits != 0" class="resultsBar"> 
    5463                <a href="${c.state.constrainedurl}"> Refine search</a>  
     64                | Order By | 
    5565                | Found ${c.state.hits} | Showing ${c.state.offset}-$upper  
    5666                  <span py:if="p!=[]"> | <a href="$purl"> Previous ${p[1]}</a> </span> 
  • MILK/trunk/milk_server/milk_server/templates/ndgPage.html

    r5261 r5302  
    22        xmlns:py="http://genshi.edgewall.org/" 
    33        xmlns:xi="http://www.w3.org/2001/XInclude" py:strip=""> 
    4  
    54        <xi:include href="utils.html" /> 
    65<?python 
     
    172171     
    173172 
    174     <div py:if="c.errors" class="error" py:def="displayErrors()"> 
     173    <div class="error" py:def="displayErrorDict(errors)"> 
    175174        <h3>Input error</h3> 
    176175        <table align="center"> 
    177         <span py:for="key, errors in c.errors.items()" py:strip=""> 
    178                 <tr py:for="i, error in enumerate(errors)"> 
    179                         <td py:if="i == 0">${Markup(key)}</td> 
     176        <span py:for="key, errors in errors.items()" py:strip=""> 
     177                <tr py:if="isinstance(errors, list)" py:for="i, error in enumerate(errors)"> 
     178                        <td py:if="i == 0"><b>${Markup(key)}</b></td> 
    180179                        <td py:if="i != 0" /> 
    181180                        <td align="left">${Markup(error)}</td> 
    182181                </tr> 
     182                <tr py:if="not isinstance(errors, list)"> 
     183                        <td><b>${Markup(key)}</b></td> 
     184                        <td align="left">${Markup(errors)}</td> 
     185                </tr> 
    183186        </span> 
    184187        </table> 
  • MILK/trunk/milk_server/milk_server/templates/utils.html

    r5261 r5302  
    11<?python 
    22 from xml.sax.saxutils import escape 
     3 import milk_server.lib.constants as constants 
    34 def et2string(x): 
    45     #use this to strip namespaces for children within text elements 
     
    1415     return s 
    1516     ?> 
     17      
    1618 <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" 
    1719        py:strip=""> 
     
    115117        </span> 
    116118    </span> 
     119     
     120     
     121        <div py:def="EditTextFieldRow(title, name, value, isEditable, type, width='auto', colspan='1')" py:strip=""> 
     122                <tr> 
     123                        <td class="cellhead" width="10%">${title}:</td> 
     124                        <div py:replace="EditTextField(name, value, isEditable, type, width='$width', colspan='$colspan')"/> 
     125                </tr> 
     126        </div> 
     127 
     128 
     129        <div py:def="EditTextField(name, value, isEditable, type, width='auto', colspan='1')" py:strip=""> 
     130                <td py:if="isEditable" class="column" width="$width" colspan="$colspan"> 
     131                        <span py:if="type == constants.TEXT_FIELD" py:strip=""> 
     132                                ${Markup(h.text_field(name, value, class_="fullWidth"))} 
     133                        </span> 
     134                        <span py:if="type == constants.TEXT_AREA" py:strip=""> 
     135                                ${Markup(h.text_area(name, value, class_="fullWidth", rows="4"))} 
     136                        </span> 
     137                </td> 
     138                <td py:if="not isEditable" class="column" width="$width" colspan="$colspan"> 
     139                        ${Markup(value)} 
     140                </td> 
     141        </div> 
     142     
    117143</html> 
Note: See TracChangeset for help on using the changeset viewer.