Mercurial > p > roundup > code
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 |
