Update of /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode In directory sc8-pr-cvs1:/tmp/cvs-serv6849 Modified Files: HTMLView.py Schema.py Validator.py VariableDecode.py __init__.py test.py Added Files: FormComponent.py Properties.py README.txt Log Message: Tons of changes * FormComponent works with Components to support forms in Webware * Bug fixes galore * Fancy example using repeating fields with non-validating submit buttons to add and remove repetitions. Something I always meant to do with FFK. * Lame layout, hard to read repeats and whatnot. Don't let that fool you though. * Awkward interface. Still learning about how that will work -- more work involving automatically generated schemas (generated off Fields, for instance), which should translate to editable objects as well. (Must investigate Zope Views) --- NEW FILE: FormComponent.py --- """ Component for use with Component.CPage, for Webware. """ from Component.CPage import ServletComponent, Component from Validator import InvalidField from FormEncode.VariableDecode import NestedVariables from FormEncode import Validator, Schema, HTMLView nested = NestedVariables() stripName = Validator.StripField('_formName_') class FormServletComponent(ServletComponent): _servletMethods = [] def __init__(self, name='form', schema=None): self.name = name self.schema = schema nameCap = name[0].upper() + name[1:] processName = 'process' + nameCap renderName = 'render' + nameCap writeName = 'write' + nameCap setattr(self, processName, self.processForm) setattr(self, renderName, self.renderForm) setattr(self, writeName, self.writeForm) self._servletMethods = self._servletMethods + \ [processName, renderName, writeName] ServletComponent.__init__(self) self._stripField = Validator.StripField(self.name) def addComponentTo(self, servlet): ServletComponent.addComponentTo(self, servlet) if self.schema is None: if not hasattr(servlet, 'schema'): if not hasattr(servlet, self.name + "Schema"): assert 0, "You must provide a schema to the constructor, or else define servlet variable 'schema' or '%sSchema'" % self.name self.schema = getattr(servlet, self.name + "Schema") else: self.schema = servlet.schema if isinstance(self.schema, type): self.schema = self.schema() self.submitButtons = [] self.suppressingButtons = [] self._initSchema(self.schema, []) def _initSchema(self, schema, names): if isinstance(schema.htmlView, HTMLView.SubmitButton): if schema.htmlView.suppressValidation: self.suppressingButtons.append((schema, names[:])) else: self.submitButtons.append((schema, names[:])) elif isinstance(schema, Schema.Schema): for name, validator in schema.fields.items(): names.append(name) self._initSchema(validator, names) names.pop() def buttonPushed(self, names, valueDict): if isinstance(valueDict, list): for i in range(len(valueDict)): found, indexes = self.buttonPushed(names, valueDict[i]) if found: indexes = [i] + indexes return True, indexes return False, None try: left = valueDict[names[0]] except KeyError: return False, None if len(names) == 1: return left, [] else: return self.buttonPushed(names[1:], left) def awakeEvent(self, trans): self._errors = None def sleepEvent(self, trans): self._errors = None def processForm(self): input = self.servlet().request().fields() input = nested.toPython(input, None) try: name, input = stripName.toPython(input, None) except Validator.InvalidField: name = None if name != self.name: return False, None returnedResult = None for button, names in self.suppressingButtons: found, indexes = self.buttonPushed(names, input) if found: returnedResult = button.htmlView.execute(self.servlet(), indexes, input) if returnedResult is not None: return "partial", returnedResult try: result = self.schema.attemptToPython(input) except InvalidField, exc: self._errors = self.recursiveApply(str, exc) print "eh:", self._errors return False, self._errors returnedResult = result for button, names in self.submitButtons: found, indexes = self.buttonPushed(names, result) if found: returnedResult = button.htmlView.execute(self.servlet(), indexes, returnedResult) return True, returnedResult def recursiveApply(self, func, exc): if isinstance(exc, str): return exc elif isinstance(exc, list): return [self.recursiveApply(func, e) for e in exc] elif isinstance(exc, dict): d = {} for key, value in exc.items(): d[key] = self.recursiveApply(func, value) return d elif isinstance(exc, Exception): try: exc.errorDict exc.errorList except AttributeError: return func(exc) if exc.errorDict is not None: return self.recursiveApply(func, exc.errorDict) elif exc.errorList is not None: return self.recursiveApply(func, exc.errorList) else: return func(exc) def renderForm(self, defaults=None, options=None, httpRequest=None): if self._errors: httpRequest = self.servlet().request().fields() view = HTMLView.Form(schema=self.schema, action=self.servlet().__class__.__name__, name=self.name) request = HTMLView.FormRequest(defaults=defaults, options=options, errors=self._errors, httpRequest=httpRequest) print self._errors return view.html(request) def writeForm(self, defaults=None, options=None, httpRequest=None): self.servlet().write(self.renderForm(defaults, options, httpRequest)) class FormComponent(Component): _componentClass = FormServletComponent --- NEW FILE: Properties.py --- name = 'FormEncode' version = ('X', 'Y', 0) docs = [] status = 'alpha' requiredPyVersion = (2, 2, 0) synopsis = """Form processing""" WebKitConfig = { 'examplePages': [ 'Register', 'Address', 'AddressList', ] } --- NEW FILE: README.txt --- To try this out: ./AppServer --AppServer.PlugInDirs='["%(WebwarePath)s", "/path/to/Sandbox/ianbicking"]' Index: HTMLView.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/HTMLView.py,v retrieving revision 1.4 retrieving revision 1.5 diff -C2 -d -r1.4 -r1.5 *** HTMLView.py 10 May 2003 09:04:20 -0000 1.4 --- HTMLView.py 20 May 2003 09:28:37 -0000 1.5 *************** *** 58,64 **** def __init__(self, httpRequest=None, defaults=None, ! options=None, state=None): self.options = options or {} self.defaults = defaults or {} self.httpRequest = httpRequest self.state = state --- 58,65 ---- def __init__(self, httpRequest=None, defaults=None, ! options=None, errors=None, state=None): self.options = options or {} self.defaults = defaults or {} + self.errors = errors self.httpRequest = httpRequest self.state = state *************** *** 67,70 **** --- 68,72 ---- self.currentDefault = self.defaults self.currentOptions = self.options + self.currentErrors = self.errors self.currentValidator = None self._encodedNameStack = [] *************** *** 72,75 **** --- 74,78 ---- self._defaultStack = [] self._optionsStack = [] + self._errorsStack = [] def pushName(self, name): *************** *** 82,88 **** self._defaultStack.append(self.currentDefault) self._optionsStack.append(self.currentOptions) if self.currentDefault is not None: self.currentDefault = self.currentDefault.get(name, None) ! self.currentOptions = self.currentOptions.get(name, {}) def popName(self): --- 85,98 ---- self._defaultStack.append(self.currentDefault) self._optionsStack.append(self.currentOptions) + self._errorsStack.append(self.currentErrors) if self.currentDefault is not None: self.currentDefault = self.currentDefault.get(name, None) ! if self.currentErrors is not None: ! try: ! self.currentErrors = self.currentErrors.get(name, None) ! except AttributeError: ! self.currentErrors = None ! if self.currentOptions is not None: ! self.currentOptions = self.currentOptions.get(name, {}) def popName(self): *************** *** 94,97 **** --- 104,108 ---- self.currentDefault = self._defaultStack.pop() self.currentOptions = self._optionsStack.pop() + self.currentErrors = self._errorsStack.pop() self._nameStack.pop() *************** *** 106,114 **** self._defaultStack.append(self.currentDefault) self._optionsStack.append(self.currentOptions) ! if self.currentDefault and isinstance(self.currentDefault, list): try: self.currentDefault = self.currentDefault[index] except IndexError: self.currentDefault = None if isinstance(self.currentOptions, list): try: --- 117,131 ---- self._defaultStack.append(self.currentDefault) self._optionsStack.append(self.currentOptions) ! self._errorsStack.append(self.currentErrors) ! if isinstance(self.currentDefault, list): try: self.currentDefault = self.currentDefault[index] except IndexError: self.currentDefault = None + if isinstance(self.currentErrors, list): + try: + self.currentErrors = self.currentErrors[index] + except IndexError: + self.currentErrors = None if isinstance(self.currentOptions, list): try: *************** *** 116,119 **** --- 133,142 ---- except IndexError: self.currentOptions = None + elif isinstance(self.currentOptions, dict) \ + and self.currentOptions.has_key('subOptions'): + try: + self.currentOptions = self.currentOptions['subOptions'][index] + except IndexError: + self.currentOptions = None def popCount(self): *************** *** 121,124 **** --- 144,150 ---- return self._popStack() + def lastName(self): + return self._nameStack[-1] + def option(self, optionName, caller, default=NoDefault): try: *************** *** 136,142 **** self.pushName(subName) if self.httpRequest: value = self.httpRequest.get(self.name) ! value = self.validator.toPython(value, self.state) else: value = self.currentDefault --- 162,177 ---- self.pushName(subName) + # @@: I'm uncomfortable that the input from Python (currentDefault) + # isn't marshalled for HTTP yet, but the HTTP input obviously + # is. But how do we know what the validator is right now, to + # do the marshalling (or unmarshalling)? + # + # Really we want to get the HTTP value at this point, most likely. + # So it's currentDefault that's wrong. It might be an integer, + # or a dictionary, and it needs to go through its validator to + # be corrected (fromPython). if self.httpRequest: value = self.httpRequest.get(self.name) ! #value = self.validator.toPython(value, self.state) else: value = self.currentDefault *************** *** 146,149 **** --- 181,187 ---- return value + def error(self): + return self.currentErrors + def subName(self, subName): self.pushName(subName) *************** *** 158,161 **** --- 196,201 ---- schema = None layout = None + name = Exclude + useFormNameField = True def __init__(self, **kw): *************** *** 174,182 **** request.enctype = Exclude inner = self.layout.createForm(self.schema, request) ! # Add form javascript result = html.form( action=self.action, enctype=request.enctype, method=self.method, c=inner) request.inForm = None --- 214,227 ---- request.enctype = Exclude inner = self.layout.createForm(self.schema, request) ! # @@: Add form javascript ! if self.name and self.name is not Exclude \ ! and self.useFormNameField: ! inner += '<input type="hidden" name="_formName_" value="%s">' \ ! % (htmlEncode(self.name)) result = html.form( action=self.action, enctype=request.enctype, method=self.method, + name=self.name, c=inner) request.inForm = None *************** *** 213,219 **** validator, request)) request.popCount() ! return '\n'.join(result) ! ! value = self.formFieldUnrepeating(validator, request) if name: request.popName() --- 258,264 ---- validator, request)) request.popCount() ! value = '\n'.join(result) ! else: ! value = self.formFieldUnrepeating(validator, request) if name: request.popName() *************** *** 230,245 **** view = validator.htmlView ! return self.formatField(view, request) def formatField(self, view, request): ! desc = view.description or '' if desc: desc = desc + ": " return html( desc, view.html(request), html.br()) ! class SortedTable(Layout): --- 275,319 ---- view = validator.htmlView ! if view.hidden: ! result = view.html(request) ! error = request.error() ! if error: ! result += self.formatError(error, request) ! return result ! else: ! return self.formatField(view, request) def formatField(self, view, request): ! desc = view.description ! if desc is None: ! desc = self.convertName(request.lastName()) if desc: desc = desc + ": " + error = request.error() or '' + if error: + error = self.formatError(error, request) return html( desc, view.html(request), + error, html.br()) ! def formatError(self, error, request): ! return '<span style="background-color: #993333; color: #ffffff">%s</span>' % error ! ! _underlineRE = re.compile('_') ! _capsRE = re.compile('[A-Z]+') ! def convertName(self, name): ! name = self._underlineRE.sub(' ', name) ! name = self._capsRE.sub(lambda m, n=name, s=self: s._convertNameSub(m, n), name) ! return name ! ! def _convertNameSub(self, match, name): ! m = match.group(0).lower() ! if len(m) > 1: ! if match.end() == len(name): ! return ' %s' % m ! return '%s %s' % (m[:-1], m[-1]) ! return ' %s' % m class SortedTable(Layout): *************** *** 271,275 **** if self._description is not None: return self._description ! return '' assert 0, "@@: auto-name based on validator or something" --- 345,349 ---- if self._description is not None: return self._description ! return None assert 0, "@@: auto-name based on validator or something" *************** *** 325,328 **** --- 399,404 ---- methodToInvoke = None + extraArgs = () + extraKW = {} invokeAsFunction = False defaultSubmit = False *************** *** 330,333 **** --- 406,410 ---- confirm = None defaultDescription = "Submit" + description = '' def htmlInput(self, request): *************** *** 352,355 **** --- 429,440 ---- return '' + def execute(self, servlet, indexes, result): + if self.methodToInvoke: + assert not self.invokeAsFunction, "Not yet implemented" + meth = getattr(servlet, self.methodToInvoke) + return meth(indexes, result, *self.extraArgs, **self.extraKW) + else: + return result + class ImageSubmit(SubmitButton): *************** *** 380,383 **** --- 465,470 ---- strings in (unless you use a converter like AsInt). """ + + hidden = True def htmlInput(self, request): Index: Schema.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/Schema.py,v retrieving revision 1.5 retrieving revision 1.6 diff -C2 -d -r1.5 -r1.6 *** Schema.py 10 May 2003 09:04:20 -0000 1.5 --- Schema.py 20 May 2003 09:28:37 -0000 1.6 *************** *** 6,9 **** --- 6,11 ---- def __new__(meta, className, bases, d): cls = type.__new__(meta, className, bases, d) + if bases == (Validator.Validator,): + return cls try: fields = cls._Schema_fields.copy() *************** *** 12,21 **** cls._Schema_fields = fields for key, value in d.items(): ! if bases != (Validator.Validator,) \ ! and isinstance(value, type) \ and issubclass(value, Validator.Validator): cls._Schema_fields[key] = value() elif isinstance(value, Validator.Validator): cls._Schema_fields[key] = value return cls --- 14,26 ---- cls._Schema_fields = fields for key, value in d.items(): ! if isinstance(value, type) \ and issubclass(value, Validator.Validator): cls._Schema_fields[key] = value() + delattr(cls, key) elif isinstance(value, Validator.Validator): cls._Schema_fields[key] = value + delattr(cls, key) + elif cls._Schema_fields.has_key(key): + del cls._Schema_fields[key] return cls *************** *** 28,31 **** --- 33,38 ---- preValidators = [] + _Schema_fields = {} + def __init__(self, **schemaDef): kw = {} *************** *** 104,111 **** if not valueDict and self.ifEmpty is not Validator.NoDefault: return self.ifEmpty - #print 'PRE', valueDict for validator in self.preValidators: valueDict = validator.toPython(valueDict, state) - #print 'POST', valueDict new = {} --- 111,116 ---- *************** *** 113,117 **** unused = self.fields.keys() for name, value in valueDict.items(): ! unused.remove(name) if self.fields[name].repeating: --- 118,126 ---- unused = self.fields.keys() for name, value in valueDict.items(): ! try: ! unused.remove(name) ! except ValueError: ! raise Validator.InvalidField(self.message('notExpected', 'The input field %(name)s was not expected.') % {'name': repr(name)}, ! valueDict, state) if self.fields[name].repeating: *************** *** 143,147 **** new[name] = self.fields[name].attemptToPython(value, state) except Validator.InvalidField, e: - raise errors[name] = e --- 152,155 ---- *************** *** 163,167 **** raise Validator.InvalidField( formatCompoundError(errors), ! value, state, errorDict=errors) --- 171,175 ---- raise Validator.InvalidField( formatCompoundError(errors), ! valueDict, state, errorDict=errors) *************** *** 179,187 **** return ('%s\n' % (' '*indent)).join( ["%s: %s" % (k, formatCompoundError(value, indent=len(k)+2)) ! for k, value in l]) elif isinstance(v, list): return ('%s\n' % (' '*indent)).join( ['%s' % (formatCompoundError(value, indent=indent)) ! for value in v]) elif isinstance(v, str): return v --- 187,197 ---- return ('%s\n' % (' '*indent)).join( ["%s: %s" % (k, formatCompoundError(value, indent=len(k)+2)) ! for k, value in l ! if value is not None]) elif isinstance(v, list): return ('%s\n' % (' '*indent)).join( ['%s' % (formatCompoundError(value, indent=indent)) ! for value in v ! if value is not None]) elif isinstance(v, str): return v Index: Validator.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/Validator.py,v retrieving revision 1.7 retrieving revision 1.8 diff -C2 -d -r1.7 -r1.8 *** Validator.py 10 May 2003 09:04:21 -0000 1.7 --- Validator.py 20 May 2003 09:28:37 -0000 1.8 *************** *** 59,63 **** def __str__(self): ! return "%s. Value: %s" % (self.msg, repr(self.value)) class InvalidInput(Exception): --- 59,66 ---- def __str__(self): ! val = str(self.msg) ! if self.value: ! val += ". Value: %s" % repr(self.value) ! return val class InvalidInput(Exception): *************** *** 730,733 **** --- 733,751 ---- kw['messages']['invalid'] = 'Please enter a zip code (5 digits)' Regex.__init__(self, r'^\d\d\d\d\d(?:-\d\d\d\d)?$', **kw) + + class StripField(Validator): + + def __init__(self, name, **kw): + self.name = name + Validator.__init__(self, **kw) + + def toPython(self, valueDict, state): + v = valueDict.copy() + try: + field = v[self.name] + del v[self.name] + except KeyError: + raise InvalidField(self.message('missing', 'The name %(name)s is missing') % {'name': repr(self.name)}, valueDict, state) + return field, v class FormValidator(Validator): Index: VariableDecode.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/VariableDecode.py,v retrieving revision 1.2 retrieving revision 1.3 diff -C2 -d -r1.2 -r1.3 *** VariableDecode.py 2 May 2003 08:05:27 -0000 1.2 --- VariableDecode.py 20 May 2003 09:28:37 -0000 1.3 *************** *** 80,83 **** --- 80,102 ---- return result + def variableEncode(d, prepend='', result=None): + if result is None: + result = {} + if isinstance(d, dict): + for key, value in d.items(): + if key is None: + name = prepend + elif not prepend: + name = key + else: + name = "%s.%s" % (prepend, key) + variableEncode(value, name, result) + elif isinstance(d, list): + for i in range(len(d)): + variableEncode(d[i], "%s-%i" % (prepend, i), result) + else: + result[prepend] = d + return result + class NestedVariables(Validator.Validator): Index: __init__.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/__init__.py,v retrieving revision 1.1 retrieving revision 1.2 diff -C2 -d -r1.1 -r1.2 *** __init__.py 7 May 2003 05:24:00 -0000 1.1 --- __init__.py 20 May 2003 09:28:37 -0000 1.2 *************** *** 1 **** ! # --- 1,2 ---- ! def InstallInWebKit(appServer): ! pass Index: test.py =================================================================== RCS file: /cvsroot/webware-sandbox/Sandbox/ianbicking/FormEncode/test.py,v retrieving revision 1.6 retrieving revision 1.7 diff -C2 -d -r1.6 -r1.7 *** test.py 10 May 2003 09:04:21 -0000 1.6 --- test.py 20 May 2003 09:28:37 -0000 1.7 *************** *** 320,347 **** class schema(NestedSchema): ! fname = Validator.String(notEmpty=True, htmlView=HTMLView.Text) ! lname = Validator.String(notEmpty=True, htmlView=HTMLView.Text) class phone(NestedSchema): repeating = 1 ! number = Validator.PhoneNumber(htmlView=HTMLView.Text) type = Validator.OneOf(['home', 'work'], ! htmlView=HTMLView.Select(selections=[('home', 'home'), ('work', 'work')])) renderings = [ (None, d(phone=d(repetitions=2)), html.form(action="form", method="POST", ! c=[html.input.text(name="fname", value=""), html.br, html.input.text(name="lname", value=""), html.br, html.input.text(name="phone-0.number", value=""), html.br, html.select(name="phone-0.type", c=[html.option("home", value="home"), html.option("work", value="work")]), html.br, html.input.text(name="phone-1.number", value=""), html.br, html.select(name="phone-1.type", c=[html.option("home", value="home"), --- 320,357 ---- class schema(NestedSchema): ! fname = Validator.String(notEmpty=True, htmlView=HTMLView.Text, ! description='') ! lname = Validator.String(notEmpty=True, htmlView=HTMLView.Text, ! description='') class phone(NestedSchema): repeating = 1 ! number = Validator.PhoneNumber(htmlView=HTMLView.Text, ! description='') type = Validator.OneOf(['home', 'work'], ! htmlView=HTMLView.Select(selections=[('home', 'home'), ('work', 'work')]), ! description='') renderings = [ (None, d(phone=d(repetitions=2)), html.form(action="form", method="POST", ! c=['fname:', ! html.input.text(name="fname", value=""), html.br, + 'lname:', html.input.text(name="lname", value=""), html.br, + 'number:', html.input.text(name="phone-0.number", value=""), html.br, + 'type:', html.select(name="phone-0.type", c=[html.option("home", value="home"), html.option("work", value="work")]), html.br, + 'number:', html.input.text(name="phone-1.number", value=""), html.br, + 'type:', html.select(name="phone-1.type", c=[html.option("home", value="home"), *************** *** 353,368 **** 'lname': 'test2'}, None, html.form(action='form', method='POST', ! c=[html.input.text(name='fname', value='test'), html.br, html.input.text(name='lname', value='test2'), html.br, html.input.text(name='phone-0.number', value='111'), html.br, html.select(name='phone-0.type', c=[html.option('home', value='home', selected='selected'), html.option('work', value='work')]), html.br, html.input.text(name='phone-1.number', value='222'), html.br, html.select(name='phone-1.type', c=[html.option('home', value='home'), --- 363,384 ---- 'lname': 'test2'}, None, html.form(action='form', method='POST', ! c=['fname:', ! html.input.text(name='fname', value='test'), html.br, + 'lname:', html.input.text(name='lname', value='test2'), html.br, + 'number:', html.input.text(name='phone-0.number', value='111'), html.br, + 'type:', html.select(name='phone-0.type', c=[html.option('home', value='home', selected='selected'), html.option('work', value='work')]), html.br, + 'number:', html.input.text(name='phone-1.number', value='222'), html.br, + 'type:', html.select(name='phone-1.type', c=[html.option('home', value='home'), *************** *** 373,378 **** phone=d(repetitions=0)), html.form(action='form', method='POST', ! c=[html.input.hidden(name='fname', value='f'), html.br, html.input.text(name='lname', value='l'), html.br])), --- 389,396 ---- phone=d(repetitions=0)), html.form(action='form', method='POST', ! c=['fname:', ! html.input.hidden(name='fname', value='f'), html.br, + 'lname:', html.input.text(name='lname', value='l'), html.br])), *************** *** 384,389 **** class schema(NestedSchema): ! username = Validator.String(notEmpty=True, htmlView=HTMLView.Text) ! password = Validator.String(notEmpty=True, htmlView=HTMLView.Password) submit = Validator.String(htmlView=HTMLView.SubmitButton) --- 402,409 ---- class schema(NestedSchema): ! username = Validator.String(notEmpty=True, ! htmlView=HTMLView.Text) ! password = Validator.String(notEmpty=True, ! htmlView=HTMLView.Password) submit = Validator.String(htmlView=HTMLView.SubmitButton) *************** *** 391,398 **** (d(username="x"), d(username=d(static=1)), html.form(action="form", method="POST", ! c=[html.input.password(name='password', value=''), html.br, html.input.submit(name='submit', value='Submit'), html.br, 'x', html.input.hidden(name='username', value='x'), --- 411,420 ---- (d(username="x"), d(username=d(static=1)), html.form(action="form", method="POST", ! c=['password:', ! html.input.password(name='password', value=''), html.br, html.input.submit(name='submit', value='Submit'), html.br, + 'username: ', 'x', html.input.hidden(name='username', value='x'), *************** *** 402,410 **** username=d(hidden=1)), html.form(action="form", method="POST", ! c=[html.input.password(name='password', value=''), html.br, html.input.submit(name='submit', value='Fun', onClick="return window.confirm('Really?')"), html.br, html.input.hidden(name='username', value=''), html.br])), --- 424,434 ---- username=d(hidden=1)), html.form(action="form", method="POST", ! c=['password:', ! html.input.password(name='password', value=''), html.br, html.input.submit(name='submit', value='Fun', onClick="return window.confirm('Really?')"), html.br, + 'username:', html.input.hidden(name='username', value=''), html.br])), *************** *** 460,465 **** CreditCardValidator ! Schema.py ! Good! VariableDecode.py: --- 484,489 ---- CreditCardValidator ! Schema.py: ! All Good! VariableDecode.py: |