[gen] Added field dict, similar to List but that stores dicts in the database, with a set of keys given in method field.keys.

This commit is contained in:
Gaetan Delannay 2015-03-16 12:08:13 +01:00
parent 424c0521de
commit 7e7d0933f5
7 changed files with 117 additions and 14 deletions

View file

@ -25,6 +25,9 @@ from group import Group
from search import Search from search import Search
from page import Page from page import Page
# In this file, names "list" and "dict" refer to sub-modules. To use Python
# builtin types, use __builtins__['list'] and __builtins__['dict']
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Field: class Field:
'''Basic abstract class for defining any field.''' '''Basic abstract class for defining any field.'''
@ -396,7 +399,7 @@ class Field:
# We must initialise the corresponding back reference # We must initialise the corresponding back reference
self.back.klass = klass self.back.klass = klass
self.back.init(self.back.attribute, self.klass, appName) self.back.init(self.back.attribute, self.klass, appName)
if self.type == "List": if self.type in ('List', 'Dict'):
for subName, subField in self.fields: for subName, subField in self.fields:
fullName = '%s_%s' % (name, subName) fullName = '%s_%s' % (name, subName)
subField.init(fullName, klass, appName) subField.init(fullName, klass, appName)
@ -490,7 +493,7 @@ class Field:
def formatMapping(self, mapping): def formatMapping(self, mapping):
'''Creates a dict of mappings, one entry by label type (label, descr, '''Creates a dict of mappings, one entry by label type (label, descr,
help).''' help).'''
if isinstance(mapping, dict): if isinstance(mapping, __builtins__['dict']):
# Is it a dict like {'label':..., 'descr':...}, or is it directly a # Is it a dict like {'label':..., 'descr':...}, or is it directly a
# dict with a mapping? # dict with a mapping?
for k, v in mapping.iteritems(): for k, v in mapping.iteritems():
@ -504,7 +507,7 @@ class Field:
mapping[labelType] = None # No mapping for this value. mapping[labelType] = None # No mapping for this value.
return mapping return mapping
else: else:
# Mapping is a method that must be applied to any i18n message. # Mapping is a method that must be applied to any i18n message
return {'label':mapping, 'descr':mapping, 'help':mapping} return {'label':mapping, 'descr':mapping, 'help':mapping}
def formatLayouts(self, layouts): def formatLayouts(self, layouts):
@ -532,7 +535,7 @@ class Field:
layouts = copy.deepcopy(layouts) layouts = copy.deepcopy(layouts)
if 'edit' not in layouts: if 'edit' not in layouts:
defEditLayout = self.computeDefaultLayouts() defEditLayout = self.computeDefaultLayouts()
if type(defEditLayout) == dict: if isinstance(defEditLayout, __builtins__['dict']):
defEditLayout = defEditLayout['edit'] defEditLayout = defEditLayout['edit']
layouts['edit'] = defEditLayout layouts['edit'] = defEditLayout
# We have now a dict of layouts in p_layouts. Ensure now that a Table # We have now a dict of layouts in p_layouts. Ensure now that a Table

98
fields/dict.py Normal file
View file

@ -0,0 +1,98 @@
# ------------------------------------------------------------------------------
# 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 <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy import Object
from list import List
from appy.px import Px
from appy.gen.layout import Table
# ------------------------------------------------------------------------------
class Dict(List):
'''A Dict is stored as a dict of Object instances [Object]~. Keys are fixed
and are given by a method specified in parameter "keys". Values are
Object instances, whose attributes are determined by parameter "fields"
that, similarly to the List field, determines sub-data for every entry in
the dict. This field is build on top of the List field.'''
# PX for rendering a single row
pxRow = Px('''
<tr valign="top" class=":loop.row.odd and 'even' or 'odd'">
<td>:row[1]</td>
<td for="info in subFields" if="info[1]" align="center"
var2="field=info[1];
fieldName='%s*%d' % (field.name, rowIndex);
tagCss='noStyle'">:field.pxRender</td>
</tr>''')
# PX for rendering the dict (shared between pxView and pxEdit)
pxTable = Px('''
<table var="isEdit=layoutType == 'edit'" if="isEdit or value"
id=":'list_%s' % name" class=":isEdit and 'grid' or 'compact list'"
width=":field.width"
var2="keys=field.keys(obj);
subFields=field.getSubFields(zobj, layoutType)">
<!-- Header -->
<tr valign="bottom">
<th></th>
<th for="info in subFields" if="info[1]"
width=":field.widths[loop.info.nb]">::_(info[1].labelId)</th>
</tr>
<!-- Rows of data -->
<x for="row in keys" var2="rowIndex=loop.row.nb">:field.pxRow</x>
</table>''')
def __init__(self, keys, fields, validator=None, multiplicity=(0,1),
default=None, show=True, page='main', group=None, layouts=None,
move=0, specificReadPermission=False,
specificWritePermission=False, width='', height=None,
maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None,
subLayouts=Table('frv', width=None), widths=None, view=None,
xml=None):
List.__init__(self, fields, validator, multiplicity, default, show, page,
group, layouts, move, specificReadPermission,
specificWritePermission, width, height, maxChars, colspan,
master, masterValue, focus, historized, mapping, label,
subLayouts, widths, view, xml)
# Method in "keys" must return a list of tuples (key, title): "key"
# determines the key that will be used to store the entry in the
# database, while "title" will get the text that will be shown in the ui
# while encoding/viewing this entry.
self.keys = keys
def getFormattedValue(self, obj, value, layoutType='view',
showChanges=False, language=None):
'''Formats the dict value as a list of values'''
res = []
for key, title in self.keys(obj.appy()):
if key in value:
res.append(value[key])
else:
# There is no value for this key in the database p_value
res.append(None)
return res
def getStorableValue(self, obj, value):
'''Gets p_value in a form that can be stored in the database'''
res = {}
values = List.getStorableValue(self, obj, value)
i = -1
for key, title in self.keys(obj.appy()):
i += 1
res[key] = values[i]
return res
# ------------------------------------------------------------------------------

View file

@ -123,14 +123,14 @@ class List(Field):
return res return res
def getRequestValue(self, obj, requestName=None): def getRequestValue(self, obj, requestName=None):
'''Concatenates the list from distinct form elements in the request.''' '''Concatenates the list from distinct form elements in the request'''
request = obj.REQUEST request = obj.REQUEST
name = requestName or self.name # A List may be into another List (?) name = requestName or self.name # A List may be into another List (?)
prefix = name + '*' + self.fields[0][0] + '*' prefix = name + '*' + self.fields[0][0] + '*'
res = {} res = {}
for key in request.keys(): for key in request.keys():
if not key.startswith(prefix): continue if not key.startswith(prefix): continue
# I have found a row. Gets its index # I have found a row. Gets its index.
row = Object() row = Object()
if '_' in key: key = key[:key.index('_')] if '_' in key: key = key[:key.index('_')]
rowIndex = int(key.split('*')[-1]) rowIndex = int(key.split('*')[-1])
@ -140,7 +140,7 @@ class List(Field):
v = subField.getRequestValue(obj, requestName=keyName) v = subField.getRequestValue(obj, requestName=keyName)
setattr(row, subName, v) setattr(row, subName, v)
res[rowIndex] = row res[rowIndex] = row
# Produce a sorted list. # Produce a sorted list
keys = res.keys() keys = res.keys()
keys.sort() keys.sort()
res = [res[key] for key in keys] res = [res[key] for key in keys]
@ -153,7 +153,7 @@ class List(Field):
return res return res
def getStorableValue(self, obj, value): def getStorableValue(self, obj, value):
'''Gets p_value in a form that can be stored in the database.''' '''Gets p_value in a form that can be stored in the database'''
res = [] res = []
for v in value: for v in value:
sv = Object() sv = Object()

View file

@ -29,6 +29,7 @@ from appy.fields.float import Float
from appy.fields.info import Info from appy.fields.info import Info
from appy.fields.integer import Integer from appy.fields.integer import Integer
from appy.fields.list import List from appy.fields.list import List
from appy.fields.dict import Dict
from appy.fields.pod import Pod from appy.fields.pod import Pod
from appy.fields.ref import Ref, autoref from appy.fields.ref import Ref, autoref
from appy.fields.string import String, Selection from appy.fields.string import String, Selection

View file

@ -319,6 +319,7 @@ class FieldDescriptor:
self.i18n('%s_descr' % label, ' ') self.i18n('%s_descr' % label, ' ')
if field.hasHelp: if field.hasHelp:
self.i18n('%s_help' % label, ' ') self.i18n('%s_help' % label, ' ')
walkDict = walkList # Same i18n labels for a dict
def walkCalendar(self): def walkCalendar(self):
# Add i18n-specific messages # Add i18n-specific messages

View file

@ -256,12 +256,12 @@ class BaseMixin:
return dbFolder, path return dbFolder, path
def view(self): def view(self):
'''Returns the view PX.''' '''Returns the view PX'''
obj = self.appy() obj = self.appy()
return obj.pxView({'obj': obj, 'tool': obj.tool}) return obj.pxView({'obj': obj, 'tool': obj.tool})
def edit(self): def edit(self):
'''Returns the edit PX.''' '''Returns the edit PX'''
obj = self.appy() obj = self.appy()
return obj.pxEdit({'obj': obj, 'tool': obj.tool}) return obj.pxEdit({'obj': obj, 'tool': obj.tool})
@ -347,8 +347,8 @@ class BaseMixin:
setattr(errors, field.name, message) setattr(errors, field.name, message)
else: else:
setattr(values, field.name, field.getStorableValue(self, value)) setattr(values, field.name, field.getStorableValue(self, value))
# Validate sub-fields within Lists # Validate sub-fields within Lists/Dicts
if field.type != 'List': continue if field.type not in ('List', 'Dict'): continue
i = -1 i = -1
for row in value: for row in value:
i += 1 i += 1
@ -710,7 +710,7 @@ class BaseMixin:
field = self.getAppyType(name) field = self.getAppyType(name)
if field.type == 'Pod': return if field.type == 'Pod': return
if '*' not in name: return field.getValue(self) if '*' not in name: return field.getValue(self)
# The field is an inner field from a List. # The field is an inner field from a List/Dict
listName, name, i = name.split('*') listName, name, i = name.split('*')
listType = self.getAppyType(listName) listType = self.getAppyType(listName)
return listType.getInnerValue(self, outerValue, name, int(i)) return listType.getInnerValue(self, outerValue, name, int(i))

View file

@ -124,7 +124,7 @@ td.search { padding-top: 8px }
.compact th, .compact td { padding: 0px 3px 0px 3px } .compact th, .compact td { padding: 0px 3px 0px 3px }
.grid th { font-style: italic; font-weight: normal; .grid th { font-style: italic; font-weight: normal;
border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px } border-bottom: 5px solid #fdfdfd; padding: 3px 5px 0 5px }
.grid td { padding: 3px 3px 0 3px } .grid td { padding: 0 3px }
.timeline { font-size: 85%; color: #555555 } .timeline { font-size: 85%; color: #555555 }
.timeline td { text-align: center; padding: 1px } .timeline td { text-align: center; padding: 1px }
.timeline th { padding: 1px } .timeline th { padding: 1px }