comparison roundup/cgi/client.py @ 1438:13c42b803101

Better handling of the form variable labels. It's done in one step rather than the split step of last week. This means that "msg-1@link@files" will work now. I had to change the password confirmation special var from "name:confirm" to ":confirm:name" so it conformed with the pattern set by ":add:" etc.
author Richard Jones <richard@users.sourceforge.net>
date Mon, 17 Feb 2003 06:44:01 +0000
parents 077235194ac2
children b42fa71754c9
comparison
equal deleted inserted replaced
1437:077235194ac2 1438:13c42b803101
1 # $Id: client.py,v 1.88 2003-02-17 01:04:31 richard Exp $ 1 # $Id: client.py,v 1.89 2003-02-17 06:44:00 richard Exp $
2 2
3 __doc__ = """ 3 __doc__ = """
4 WWW request handler (also used in the stand-alone server). 4 WWW request handler (also used in the stand-alone server).
5 """ 5 """
6 6
91 # 91 #
92 FV_TEMPLATE = re.compile(r'[@:]template') 92 FV_TEMPLATE = re.compile(r'[@:]template')
93 FV_OK_MESSAGE = re.compile(r'[@:]ok_message') 93 FV_OK_MESSAGE = re.compile(r'[@:]ok_message')
94 FV_ERROR_MESSAGE = re.compile(r'[@:]error_message') 94 FV_ERROR_MESSAGE = re.compile(r'[@:]error_message')
95 95
96 # specials for parsePropsFromForm 96 # edit form variable handling (see unit tests)
97 FV_REQUIRED = re.compile(r'[@:]required') 97 FV_LABELS = r'''
98 FV_ADD = re.compile(r'([@:])add\1') 98 ^(
99 FV_REMOVE = re.compile(r'([@:])remove\1') 99 (?P<note>[@:]note)|
100 FV_CONFIRM = re.compile(r'.+[@:]confirm') 100 (?P<file>[@:]file)|
101 FV_LINK = re.compile(r'([@:])link\1(.+)') 101 (
102 102 ((?P<classname>%s)(?P<id>[-\d]+))? # optional leading designator
103 # deprecated 103 ((?P<required>[@:]required$)| # :required
104 FV_NOTE = re.compile(r'[@:]note') 104 (
105 FV_FILE = re.compile(r'[@:]file') 105 (
106 (?P<add>[@:]add[@:])| # :add:<prop>
107 (?P<remove>[@:]remove[@:])| # :remove:<prop>
108 (?P<confirm>[@:]confirm[@:])| # :confirm:<prop>
109 (?P<link>[@:]link[@:])| # :link:<prop>
110 ([@:]) # just a separator
111 )?
112 (?P<propname>[^@:]+) # <prop>
113 )
114 )
115 )
116 )$'''
106 117
107 # Note: index page stuff doesn't appear here: 118 # Note: index page stuff doesn't appear here:
108 # columns, sort, sortdir, filter, group, groupdir, search_text, 119 # columns, sort, sortdir, filter, group, groupdir, search_text,
109 # pagesize, startwith 120 # pagesize, startwith
110 121
1095 # create the node and return its id 1106 # create the node and return its id
1096 cl = self.db.classes[cn] 1107 cl = self.db.classes[cn]
1097 return cl.create(**props) 1108 return cl.create(**props)
1098 1109
1099 def parsePropsFromForm(self, num_re=re.compile('^\d+$')): 1110 def parsePropsFromForm(self, num_re=re.compile('^\d+$')):
1100 ''' Pull properties for the given class out of the form. 1111 ''' Pull properties out of the form.
1101 1112
1102 In the following, <bracketed> values are variable, ":" may be 1113 In the following, <bracketed> values are variable, ":" may be
1103 one of ":" or "@", and other text "required" is fixed. 1114 one of ":" or "@", and other text "required" is fixed.
1104 1115
1105 Properties are specified as form variables 1116 Properties are specified as form variables:
1117
1118 <propname>
1119 - property on the current context item
1120
1106 <designator>:<propname> 1121 <designator>:<propname>
1107 1122 - property on the indicated item
1108 where the propery belongs to the context class or item if the 1123
1109 designator is not specified. The designator may specify a 1124 <classname>-<N>:<propname>
1110 negative item id value (ie. "issue-1") and a new item of the 1125 - property on the Nth new item of classname
1111 specified class will be created for each negative id found. 1126
1112 1127 Once we have determined the "propname", we check to see if it
1113 If a "<designator>:required" parameter is supplied, 1128 is one of the special form values:
1114 then the named property values must be supplied or a 1129
1115 ValueError will be raised. 1130 :required
1116 1131 The named property values must be supplied or a ValueError
1117 Other special form values: 1132 will be raised.
1118 [classname|designator]:remove:<propname>=id(s) 1133
1134 :remove:<propname>=id(s)
1119 The ids will be removed from the multilink property. 1135 The ids will be removed from the multilink property.
1120 [classname|designator]:add:<propname>=id(s) 1136
1137 :add:<propname>=id(s)
1121 The ids will be added to the multilink property. 1138 The ids will be added to the multilink property.
1122 1139
1123 [classname|designator]:link:<propname>=<classname> 1140 :link:<propname>=<designator>
1124 Used to add a link to new items created during edit. 1141 Used to add a link to new items created during edit.
1125 These are collected up and returned in all_links. This will 1142 These are collected up and returned in all_links. This will
1126 result in an additional linking operation (either Link set or 1143 result in an additional linking operation (either Link set or
1127 Multilink append) after the edit/create is done using 1144 Multilink append) after the edit/create is done using
1128 all_props in _editnodes. The <propname> on 1145 all_props in _editnodes. The <propname> on the current item
1129 [classname|designator] will be set/appended the id of the 1146 will be set/appended the id of the newly created item of
1130 newly created item of class <classname>. 1147 class <designator> (where <designator> must be
1131 1148 <classname>-<N>).
1132 Note: the colon may be either ":" or "@".
1133 1149
1134 Any of the form variables may be prefixed with a classname or 1150 Any of the form variables may be prefixed with a classname or
1135 designator. 1151 designator.
1136 1152
1137 The return from this method is a dict of 1153 The return from this method is a dict of
1147 they are valid for the class). 1163 they are valid for the class).
1148 1164
1149 Two special form values are supported for backwards 1165 Two special form values are supported for backwards
1150 compatibility: 1166 compatibility:
1151 :note - create a message (with content, author and date), link 1167 :note - create a message (with content, author and date), link
1152 to the context item 1168 to the context item. This is ALWAYS desginated "msg-1".
1153 :file - create a file, attach to the current item and any 1169 :file - create a file, attach to the current item and any
1154 message created by :note 1170 message created by :note. This is ALWAYS designated
1171 "file-1".
1155 ''' 1172 '''
1156 # some very useful variables 1173 # some very useful variables
1157 db = self.db 1174 db = self.db
1158 form = self.form 1175 form = self.form
1159 1176
1160 if not hasattr(self, 'FV_ITEMSPEC'): 1177 if not hasattr(self, 'FV_SPECIAL'):
1161 # generate the regexp for detecting 1178 # generate the regexp for handling special form values
1162 # <classname|designator>[@:+]property
1163 classes = '|'.join(db.classes.keys()) 1179 classes = '|'.join(db.classes.keys())
1164 self.FV_ITEMSPEC = re.compile(r'(%s)([-\d]+)[@:](.+)$'%classes) 1180 # specials for parsePropsFromForm
1181 # handle the various forms (see unit tests)
1182 self.FV_SPECIAL = re.compile(self.FV_LABELS%classes, re.VERBOSE)
1165 self.FV_DESIGNATOR = re.compile(r'(%s)([-\d]+)'%classes) 1183 self.FV_DESIGNATOR = re.compile(r'(%s)([-\d]+)'%classes)
1166 1184
1167 # these indicate the default class / item 1185 # these indicate the default class / item
1168 default_cn = self.classname 1186 default_cn = self.classname
1169 default_cl = self.db.classes[default_cn] 1187 default_cl = self.db.classes[default_cn]
1179 all_props[(default_cn, default_nodeid)] = {} 1197 all_props[(default_cn, default_nodeid)] = {}
1180 1198
1181 keys = form.keys() 1199 keys = form.keys()
1182 timezone = db.getUserTimezone() 1200 timezone = db.getUserTimezone()
1183 1201
1202 # sentinels for the :note and :file props
1203 have_note = have_file = 0
1204
1205 # extract the usable form labels from the form
1206 matches = []
1184 for key in keys: 1207 for key in keys:
1185 # see if this value modifies a different item to the default 1208 m = self.FV_SPECIAL.match(key)
1186 m = self.FV_ITEMSPEC.match(key)
1187 if m: 1209 if m:
1210 matches.append((key, m.groupdict()))
1211
1212 # now handle the matches
1213 for key, d in matches:
1214 if d['classname']:
1188 # we got a designator 1215 # we got a designator
1189 cn = m.group(1) 1216 cn = d['classname']
1190 cl = self.db.classes[cn] 1217 cl = self.db.classes[cn]
1191 nodeid = m.group(2) 1218 nodeid = d['id']
1192 propname = m.group(3) 1219 propname = d['propname']
1193 elif key == ':note': 1220 elif d['note']:
1194 # backwards compatibility: the special note field 1221 # the special note field
1195 cn = 'msg' 1222 cn = 'msg'
1196 cl = self.db.classes[cn] 1223 cl = self.db.classes[cn]
1197 nodeid = '-1' 1224 nodeid = '-1'
1198 propname = 'content' 1225 propname = 'content'
1199 all_links.append((default_cn, default_nodeid, 'messages', 1226 all_links.append((default_cn, default_nodeid, 'messages',
1200 [('msg', '-1')])) 1227 [('msg', '-1')]))
1201 elif key == ':file': 1228 have_note = 1
1202 # backwards compatibility: the special file field 1229 elif d['file']:
1230 # the special file field
1203 cn = 'file' 1231 cn = 'file'
1204 cl = self.db.classes[cn] 1232 cl = self.db.classes[cn]
1205 nodeid = '-1' 1233 nodeid = '-1'
1206 propname = 'content' 1234 propname = 'content'
1207 all_links.append((default_cn, default_nodeid, 'files', 1235 all_links.append((default_cn, default_nodeid, 'files',
1208 [('file', '-1')])) 1236 [('file', '-1')]))
1209 if self.form.has_key(':note'): 1237 have_file = 1
1210 all_links.append(('msg', '-1', 'files', [('file', '-1')]))
1211 else: 1238 else:
1212 # default 1239 # default
1213 cn = default_cn 1240 cn = default_cn
1214 cl = default_cl 1241 cl = default_cl
1215 nodeid = default_nodeid 1242 nodeid = default_nodeid
1216 propname = key 1243 propname = d['propname']
1217 1244
1218 # the thing this value relates to is... 1245 # the thing this value relates to is...
1219 this = (cn, nodeid) 1246 this = (cn, nodeid)
1220
1221 # is this a link command?
1222 if self.FV_LINK.match(propname):
1223 value = []
1224 for entry in extractFormList(form[key]):
1225 m = self.FV_DESIGNATOR.match(entry)
1226 if not m:
1227 raise ValueError, \
1228 'link "%s" value "%s" not a designator'%(key, entry)
1229 value.append((m.groups(1), m.groups(2)))
1230 all_links.append((cn, nodeid, propname[6:], value))
1231 1247
1232 # get more info about the class, and the current set of 1248 # get more info about the class, and the current set of
1233 # form props for it 1249 # form props for it
1234 if not all_propdef.has_key(cn): 1250 if not all_propdef.has_key(cn):
1235 all_propdef[cn] = cl.getprops() 1251 all_propdef[cn] = cl.getprops()
1236 propdef = all_propdef[cn] 1252 propdef = all_propdef[cn]
1237 if not all_props.has_key(this): 1253 if not all_props.has_key(this):
1238 all_props[this] = {} 1254 all_props[this] = {}
1239 props = all_props[this] 1255 props = all_props[this]
1240 1256
1257 # is this a link command?
1258 if d['link']:
1259 value = []
1260 for entry in extractFormList(form[key]):
1261 m = self.FV_DESIGNATOR.match(entry)
1262 if not m:
1263 raise ValueError, \
1264 'link "%s" value "%s" not a designator'%(key, entry)
1265 value.append((m.group(1), m.group(2)))
1266 all_links.append((cn, nodeid, propname, value))
1267 continue
1268
1241 # detect the special ":required" variable 1269 # detect the special ":required" variable
1242 if self.FV_REQUIRED.match(key): 1270 if d['required']:
1243 all_required[this] = extractFormList(form[key]) 1271 all_required[this] = extractFormList(form[key])
1244 continue 1272 continue
1245 1273
1246 # get the required values list 1274 # get the required values list
1247 if not all_required.has_key(this): 1275 if not all_required.has_key(this):
1248 all_required[this] = [] 1276 all_required[this] = []
1249 required = all_required[this] 1277 required = all_required[this]
1250 1278
1251 # see if we're performing a special multilink action 1279 # see if we're performing a special multilink action
1252 mlaction = 'set' 1280 mlaction = 'set'
1253 if self.FV_REMOVE.match(propname): 1281 if d['remove']:
1254 propname = propname[8:]
1255 mlaction = 'remove' 1282 mlaction = 'remove'
1256 elif self.FV_ADD.match(propname): 1283 elif d['add']:
1257 propname = propname[5:]
1258 mlaction = 'add' 1284 mlaction = 'add'
1259 1285
1260 # does the property exist? 1286 # does the property exist?
1261 if not propdef.has_key(propname): 1287 if not propdef.has_key(propname):
1262 if mlaction != 'set': 1288 if mlaction != 'set':
1263 raise ValueError, 'You have submitted a %s action for'\ 1289 raise ValueError, 'You have submitted a %s action for'\
1264 ' the property "%s" which doesn\'t exist'%(mlaction, 1290 ' the property "%s" which doesn\'t exist'%(mlaction,
1265 propname) 1291 propname)
1292 # the form element is probably just something we don't care
1293 # about - ignore it
1266 continue 1294 continue
1267 proptype = propdef[propname] 1295 proptype = propdef[propname]
1268 1296
1269 # Get the form value. This value may be a MiniFieldStorage or a list 1297 # Get the form value. This value may be a MiniFieldStorage or a list
1270 # of MiniFieldStorages. 1298 # of MiniFieldStorages.
1283 # nope, pull out the value and strip it 1311 # nope, pull out the value and strip it
1284 value = value.value.strip() 1312 value = value.value.strip()
1285 1313
1286 # now that we have the props field, we need a teensy little 1314 # now that we have the props field, we need a teensy little
1287 # extra bit of help for the old :note field... 1315 # extra bit of help for the old :note field...
1288 if key == ':note' and value: 1316 if d['note'] and value:
1289 props['author'] = self.db.getuid() 1317 props['author'] = self.db.getuid()
1290 props['date'] = date.Date() 1318 props['date'] = date.Date()
1291 1319
1292 # handle by type now 1320 # handle by type now
1293 if isinstance(proptype, hyperdb.Password): 1321 if isinstance(proptype, hyperdb.Password):
1294 if not value: 1322 if not value:
1295 # ignore empty password values 1323 # ignore empty password values
1296 continue 1324 continue
1297 for key in keys: 1325 for key, d in matches:
1298 if self.FV_CONFIRM.match(key): 1326 if d['confirm'] and d['propname'] == propname:
1299 confirm = form[key] 1327 confirm = form[key]
1300 break 1328 break
1301 else: 1329 else:
1302 raise ValueError, 'Password and confirmation text do '\ 1330 raise ValueError, 'Password and confirmation text do '\
1303 'not match' 1331 'not match'
1464 1492
1465 # register this as received if required? 1493 # register this as received if required?
1466 if propname in required and value is not None: 1494 if propname in required and value is not None:
1467 required.remove(propname) 1495 required.remove(propname)
1468 1496
1497 # check to see if we need to specially link a file to the note
1498 if have_note and have_file:
1499 all_links.append(('msg', '-1', 'files', [('file', '-1')]))
1500
1469 # see if all the required properties have been supplied 1501 # see if all the required properties have been supplied
1470 s = [] 1502 s = []
1471 for thing, required in all_required.items(): 1503 for thing, required in all_required.items():
1472 if not required: 1504 if not required:
1473 continue 1505 continue

Roundup Issue Tracker: http://roundup-tracker.org/