# ------------------------------------------------------------------------------ # This file is part of Appy, a framework for building applications in the Python # language. Copyright (C) 2007 Gaetan Delannay # Appy is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation; either version 3 of the License, or (at your option) any later # version. # Appy is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with # Appy. If not, see . # ------------------------------------------------------------------------------ from appy.px import Px from appy.gen import utils as gutils # ------------------------------------------------------------------------------ class Group: '''Used for describing a group of fields within a page.''' def __init__(self, name, columns=['100%'], wide=True, style='section2', hasLabel=True, hasDescr=False, hasHelp=False, hasHeaders=False, group=None, colspan=1, align='center', valign='top', css_class='', master=None, masterValue=None, cellpadding=1, cellspacing=1, cellgap='0.6em', label=None, translated=None): self.name = name # In its simpler form, field "columns" below can hold a list or tuple # of column widths expressed as strings, that will be given as is in # the "width" attributes of the corresponding "td" tags. Instead of # strings, within this list or tuple, you may give Column instances # (see below). self.columns = columns self._setColumns() # If field "wide" below is True, the HTML table corresponding to this # group will have width 100%. You can also specify some string value, # which will be used for HTML param "width". if wide == True: self.wide = '100%' elif isinstance(wide, basestring): self.wide = wide else: self.wide = '' # If style = 'fieldset', all widgets within the group will be rendered # within an HTML fieldset. If style is 'section1' or 'section2', widgets # will be rendered after the group title. self.style = style # If hasLabel is True, the group will have a name and the corresponding # i18n label will be generated. self.hasLabel = hasLabel # If hasDescr is True, the group will have a description and the # corresponding i18n label will be generated. self.hasDescr = hasDescr # If hasHelp is True, the group will have a help text associated and the # corresponding i18n label will be generated. self.hasHelp = hasHelp # If hasheaders is True, group content will begin with a row of headers, # and a i18n label will be generated for every header. self.hasHeaders = hasHeaders self.nbOfHeaders = len(columns) # If this group is himself contained in another group, the following # attribute is filled. self.group = Group.get(group) # If the group is rendered into another group, we can specify the number # of columns that this group will span. self.colspan = colspan self.align = align self.valign = valign self.cellpadding = cellpadding self.cellspacing = cellspacing # Beyond standard cellpadding and cellspacing, cellgap can define an # additional horizontal gap between cells in a row. So this value does # not add space before the first cell or after the last one. self.cellgap = cellgap if style == 'tabs': # Group content will be rendered as tabs. In this case, some # param combinations have no sense. self.hasLabel = self.hasDescr = self.hasHelp = False # The rendering is forced to a single column self.columns = self.columns[:1] # Header labels will be used as labels for the tabs. self.hasHeaders = True self.css_class = css_class self.master = master self.masterValue = gutils.initMasterValue(masterValue) if master: master.slaves.append(self) self.label = label # See similar attr of Type class. # If a translated name is already given here, we will use it instead of # trying to translate the group label. self.translated = translated def _setColumns(self): '''Standardizes field "columns" as a list of Column instances. Indeed, the initial value for field "columns" may be a list or tuple of Column instances or strings.''' for i in range(len(self.columns)): columnData = self.columns[i] if not isinstance(columnData, Column): self.columns[i] = Column(self.columns[i]) @staticmethod def get(groupData): '''Produces a Group instance from p_groupData. User-defined p_groupData can be a string or a Group instance; this method returns always a Group instance.''' res = groupData if res and isinstance(res, basestring): # Group data is given as a string. 2 more possibilities: # (a) groupData is simply the name of the group; # (b) groupData is of the form _. groupElems = groupData.rsplit('_', 1) if len(groupElems) == 1: res = Group(groupElems[0]) else: try: nbOfColumns = int(groupElems[1]) except ValueError: nbOfColumns = 1 width = 100.0 / nbOfColumns res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns) return res def getMasterData(self): '''Gets the master of this group (and masterValue) or, recursively, of containing groups when relevant.''' if self.master: return (self.master, self.masterValue) if self.group: return self.group.getMasterData() def generateLabels(self, messages, classDescr, walkedGroups, forSearch=False): '''This method allows to generate all the needed i18n labels related to this group. p_messages is the list of i18n p_messages (a PoMessages instance) that we are currently building; p_classDescr is the descriptor of the class where this group is defined. If p_forSearch is True, this group is used for grouping searches, and not fields.''' # A part of the group label depends on p_forSearch. if forSearch: gp = 'searchgroup' else: gp = 'group' if self.hasLabel: msgId = '%s_%s_%s' % (classDescr.name, gp, self.name) messages.append(msgId, self.name) if self.hasDescr: msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name) messages.append(msgId, ' ', nice=False) if self.hasHelp: msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name) messages.append(msgId, ' ', nice=False) if self.hasHeaders: for i in range(self.nbOfHeaders): msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1) messages.append(msgId, ' ', nice=False) walkedGroups.add(self) if self.group and (self.group not in walkedGroups) and \ not self.group.label: # We remember walked groups for avoiding infinite recursion. self.group.generateLabels(messages, classDescr, walkedGroups, forSearch=forSearch) def insertInto(self, fields, uiGroups, page, metaType, forSearch=False): '''Inserts the UiGroup instance corresponding to this Group instance into p_fields, the recursive structure used for displaying all fields in a given p_page (or all searches), and returns this UiGroup instance.''' # First, create the corresponding UiGroup if not already in p_uiGroups. if self.name not in uiGroups: uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType, forSearch=forSearch) # Insert the group at the higher level (ie, directly in p_fields) # if the group is not itself in a group. if not self.group: fields.append(uiGroup) else: outerGroup = self.group.insertInto(fields, uiGroups, page, metaType,forSearch=forSearch) outerGroup.addField(uiGroup) else: uiGroup = uiGroups[self.name] return uiGroup class Column: '''Used for describing a column within a Group like defined above.''' def __init__(self, width, align="left"): self.width = width self.align = align class UiGroup: '''On-the-fly-generated data structure that groups all fields sharing the same appy.fields.Group instance, that some logged user can see.''' # PX that renders a help icon for a group. pxHelp = Px('''''') # PX that renders the content of a group. pxContent = Px('''
::_(field.labelId):field.pxHelp
::_(field.descrId)
::field.hasHeaders and \ _('%s_col%d' % (field.labelId, (colNb+1))) or ''
:field.pxView :field.pxRender
''') # PX that renders a group of fields. pxView = Px('''
::_(field.labelId)>:field.pxHelp
::_(field.descrId)
:field.pxContent
:field.pxContent
:_('%s_col%d' % (field.labelId, rowNb))
:field.pxView :field.pxRender
''') # PX that renders a group of searches. pxViewSearches = Px('''
:_(field.labelId) :field.translated
:field.pxViewSearches :search.pxView
''') def __init__(self, group, page, metaType, forSearch=False): self.type = 'group' # All p_group attributes become self attributes. for name, value in group.__dict__.iteritems(): if not name.startswith('_'): setattr(self, name, value) self.columnsWidths = [col.width for col in group.columns] self.columnsAligns = [col.align for col in group.columns] # Names of i18n labels labelName = self.name prefix = metaType if group.label: if isinstance(group.label, basestring): prefix = group.label else: # It is a tuple (metaType, name) if group.label[1]: labelName = group.label[1] if group.label[0]: prefix = group.label[0] if forSearch: gp = 'searchgroup' else: gp = 'group' self.labelId = '%s_%s_%s' % (prefix, gp, labelName) self.descrId = self.labelId + '_descr' self.helpId = self.labelId + '_help' # The name of the page where the group lies self.page = page.name # The fields belonging to the group that the current user may see. # They will be stored by m_addField below as a list of lists because # they will be rendered as a table. self.fields = [[]] # PX to user for rendering this group. self.px = forSearch and self.pxViewSearches or self.pxView def addField(self, field): '''Adds p_field into self.fields. We try first to add p_field into the last row. If it is not possible, we create a new row.''' # Get the last row lastRow = self.fields[-1] numberOfColumns = len(self.columnsWidths) # Compute the number of columns already filled in the last row. filledColumns = 0 for rowField in lastRow: filledColumns += rowField.colspan freeColumns = numberOfColumns - filledColumns if freeColumns >= field.colspan: # We can add the widget in the last row. lastRow.append(field) else: if freeColumns: # Terminate the current row by appending empty cells for i in range(freeColumns): lastRow.append('') # Create a new row self.fields.append([field]) # ------------------------------------------------------------------------------