Ejemplo n.º 1
0
 class AnotherForm(Form):
     field3 = CharField(max_length=20, widget=MyWidget3())
Ejemplo n.º 2
0
class LoginForm(Form):
    username = CharField(label="User Name", max_length=64)
    password = CharField(widget=PasswordInput())
Ejemplo n.º 3
0
 def test_null_characters_prohibited(self):
     f = CharField()
     msg = 'Null characters are not allowed.'
     with self.assertRaisesMessage(ValidationError, msg):
         f.clean('\x00something')
Ejemplo n.º 4
0
class EmailForm(Form):
    email_address = CharField(widget=EmailInput(
        attrs={'class': 'form-control'}))
Ejemplo n.º 5
0
 def __init__(self, required=True, help_text=None, **kwargs):
     self.field = CharField(required=required,
                            help_text=help_text,
                            widget=MathJaxWidget())
     super(MathBlock, self).__init__(**kwargs)
Ejemplo n.º 6
0
class ScriptForm(ModelForm):

    script_type = CharField(label='Script type',
                            required=False,
                            help_text='Script type',
                            initial=str(SCRIPT_TYPE.TESTING))

    hardware_type = CharField(
        label='Hardware type',
        required=False,
        help_text='The hardware type the script configures or tests.',
        initial=str(HARDWARE_TYPE.NODE))

    parallel = CharField(
        label='Parallel',
        required=False,
        help_text='Whether the script may run in parallel with other scripts.',
        initial=str(SCRIPT_PARALLEL.DISABLED))

    packages = CharField(label='Packages',
                         required=False,
                         help_text='Packages to be installed with script.',
                         initial='')

    timeout = DurationField(label='Timeout',
                            required=False,
                            help_text='Timeout',
                            initial=timedelta(0))

    script = VersionedTextFileField(label='Script', help_text='Script content')

    comment = CharField(label='Comment',
                        required=False,
                        help_text='Description of change',
                        initial='')

    for_hardware = CharField(
        label='For hardware',
        required=False,
        help_text='Hardware identifiers this script requires to run.',
        initial='')

    apply_configured_networking = BooleanField(required=False)

    class Meta:
        model = Script
        fields = (
            'name',
            'title',
            'description',
            'tags',
            'script_type',
            'hardware_type',
            'parallel',
            'packages',
            'timeout',
            'destructive',
            'script',
            'for_hardware',
            'may_reboot',
            'recommission',
            'apply_configured_networking',
        )

    def __init__(self, instance=None, data=None, edit_default=False, **kwargs):
        self.edit_default = edit_default
        if instance is None:
            script_data_key = 'data'
        else:
            script_data_key = 'new_data'

        data = data.copy()
        if 'comment' in data and 'script' in data:
            script_data = {
                'comment': data.get('comment'),
                script_data_key: data.get('script'),
            }
            data['script'] = script_data
            data.pop('comment')
        # Alias type to script_type to allow for consistent naming in the API.
        if 'type' in data and 'script_type' not in data:
            data['script_type'] = data['type']
            # self.data is a QueryDict. pop returns a list containing the value
            # while directly accessing it returns just the value.
            data.pop('type')

        super().__init__(instance=instance, data=data, **kwargs)

        if instance is None:
            for field in ['name', 'script']:
                self.fields[field].required = True
        else:
            for field in ['name', 'script']:
                self.fields[field].required = False
            self.fields['script'].initial = instance.script

        # Reading the embedded YAML must happen at the end of initialization
        # so the fields set are validated.
        if 'script' in self.data:
            self._read_script()

    def _validate_results(self, results={}):
        valid = True
        if isinstance(results, list):
            for result in results:
                if not isinstance(result, str):
                    set_form_error(
                        self, 'results',
                        'Each result in a result list must be a string.')
                    valid = False
        elif isinstance(results, dict):
            for result in results.values():
                if not isinstance(result, dict):
                    set_form_error(
                        self, 'results',
                        'Each result in a result dictionary must be a '
                        'dictionary.')
                elif 'title' not in result:
                    set_form_error(
                        self, 'results',
                        'title must be included in a result dictionary.')
                    valid = False
                else:
                    for key in ['title', 'description']:
                        if key in result and not isinstance(result[key], str):
                            set_form_error(self, 'results',
                                           '%s must be a string.' % key)
                            valid = False
        else:
            set_form_error(
                self, 'results',
                'results must be a list of strings or a dictionary of '
                'dictionaries.')
            valid = False
        return valid

    def _clean_script(self, parsed_yaml):
        """Clean script data and validate input."""
        # Tags and timeout may not be updated from new embedded YAML. This
        # allows users to receive updated scripts from an upstream maintainer,
        # such as Canonical, while maintaining user defined tags and timeout.

        # Tags must be a comma seperated string for the form.
        tags = parsed_yaml.pop('tags', None)
        if (tags is not None and self.instance.id is None
                and 'tags' not in self.data):
            tags_valid = True
            if isinstance(tags, str):
                self.data['tags'] = tags
            elif isinstance(tags, list):
                for tag in tags:
                    if not isinstance(tag, str):
                        tags_valid = False
                        continue
                if tags_valid:
                    self.data['tags'] = ','.join(tags)
            else:
                tags_valid = False
            if not tags_valid:
                set_form_error(
                    self, 'tags',
                    'Embedded tags must be a string of comma seperated '
                    'values, or a list of strings.')

        # Timeout must be a string for the form.
        timeout = parsed_yaml.pop('timeout', None)
        if (timeout is not None and self.instance.id is None
                and 'timeout' not in self.data):
            self.data['timeout'] = str(timeout)

        # Packages and for_hardware must be a JSON string for the form.
        for key in ['packages', 'for_hardware']:
            value = parsed_yaml.pop(key, None)
            if value is not None and key not in self.data:
                self.data[key] = json.dumps(value)

        for key, value in parsed_yaml.items():
            if key in self.fields:
                error = False
                if key not in self.data:
                    self.data[key] = value
                elif key == 'script_type':
                    # The deprecated Commissioning API always sets the
                    # script_type to commissioning as it has always only
                    # accepted commissioning scripts while the form sets
                    # the default type to testing. If the YAML matches the
                    # type allow it.
                    try:
                        if (translate_script_type(value) !=
                                translate_script_type(self.data[key])):
                            error = True
                    except ValidationError:
                        error = True
                elif value != self.data[key]:
                    # Only allow form data for fields defined in the YAML if
                    # the data matches.
                    error = True

                if error:
                    set_form_error(
                        self, key,
                        'May not override values defined in embedded YAML.')

    def _read_script(self):
        """Read embedded YAML configuration in a script.

        Search for supported MAAS script metadata in the script and
        read the values. Leading '#' are ignored. If the values are
        fields they will be entered in the form.
        """
        yaml_delim = re.compile(
            '\s*#\s*-+\s*(Start|End) MAAS (?P<version>\d+\.\d+) '
            'script metadata\s+-+', re.I)
        found_version = None
        yaml_content = ''

        if isinstance(self.data['script'], dict):
            if 'new_data' in self.data['script']:
                script = self.data['script']['new_data']
            else:
                script = self.data['script']['data']
        else:
            script = self.data['script']

        script_splitlines = script.splitlines()
        if (len(script_splitlines) >= 1
                and not script_splitlines[0].startswith('#!/')):
            set_form_error(self, 'script', 'Must start with shebang.')

        for line in script_splitlines[1:]:
            m = yaml_delim.search(line)
            if m is not None:
                if found_version is None and m.group('version') == '1.0':
                    # Found the start of the embedded YAML
                    found_version = m.group('version')
                    continue
                elif found_version == m.group('version'):
                    # Found the end of the embedded YAML
                    break
            elif found_version is not None and line.strip() != '':
                # Capture all lines inbetween the deliminator
                if '#' not in line:
                    set_form_error(self, 'script', 'Missing "#" on YAML line.')
                    return
                yaml_content += '%s\n' % line.split('#', 1)[1]

        try:
            parsed_yaml = yaml.safe_load(yaml_content)
        except yaml.YAMLError as err:
            set_form_error(self, 'script', 'Invalid YAML: %s' % err)
            return

        if not isinstance(parsed_yaml, dict):
            return

        self.instance.results = parsed_yaml.pop('results', {})
        self.instance.parameters = parsed_yaml.pop('parameters', {})

        self._clean_script(parsed_yaml)

    def clean_packages(self):
        if self.cleaned_data['packages'] == '':
            return self.instance.packages
        else:
            packages = json.loads(self.cleaned_data['packages'])

            # Automatically convert into a list incase only one package is
            # needed.
            for key in ['apt', 'snap', 'url']:
                if key in packages and not isinstance(packages[key], list):
                    packages[key] = [packages[key]]

            for key in ['apt', 'url']:
                if key in packages:
                    for package in packages[key]:
                        if not isinstance(package, str):
                            set_form_error(
                                self, 'packages',
                                'Each %s package must be a string.' % key)
            if 'snap' in packages:
                for package in packages['snap']:
                    if isinstance(package, dict):
                        if ('name' not in package
                                or not isinstance(package['name'], str)):
                            set_form_error(
                                self, 'packages',
                                'Snap package name must be defined.')
                        if ('channel' in package
                                and package['channel'] not in [
                                    'stable', 'edge', 'beta', 'candidate'
                                ]):
                            set_form_error(
                                self, 'packages',
                                'Snap channel must be stable, edge, beta, '
                                'or candidate.')
                        if ('mode' in package and package['mode']
                                not in ['classic', 'dev', 'jail']):
                            set_form_error(
                                self, 'packages',
                                'Snap mode must be classic, dev, or jail.')
                    elif not isinstance(package, str):
                        set_form_error(self, 'packages',
                                       'Snap package must be a string.')
            return packages

    def clean_for_hardware(self):
        """Convert from JSON and validate for_hardware input."""
        if self.cleaned_data['for_hardware'] == '':
            return self.instance.for_hardware
        try:
            for_hardware = json.loads(self.cleaned_data['for_hardware'])
        except JSONDecodeError:
            for_hardware = self.cleaned_data['for_hardware']
        if isinstance(for_hardware, str):
            for_hardware = for_hardware.split(',')
        if not isinstance(for_hardware, list):
            set_form_error(self, 'for_hardware', 'Must be a list or string')
            return
        regex = re.compile(
            '^modalias:.+|pci:[\da-f]{4}:[\da-f]{4}|'
            'usb:[\da-f]{4}:[\da-f]{4}|'
            'system_vendor:.*|'
            'system_product:.*|'
            'system_version:.*|'
            'mainboard_vendor:.*|'
            'mainboard_product:.*$', re.I)
        for hw_id in for_hardware:
            if regex.search(hw_id) is None:
                set_form_error(
                    self, 'for_hardware',
                    "Hardware identifier '%s' must be a modalias, PCI ID, "
                    "USB ID, system vendor, system product, system version, "
                    "mainboard vendor, or mainboard product." % hw_id)
        return for_hardware

    def clean(self):
        cleaned_data = super().clean()
        # If a field wasn't passed in keep the old values when updating.
        if self.instance.id is not None:
            for field in self._meta.fields:
                if field not in self.data:
                    cleaned_data[field] = getattr(self.instance, field)

        script_type = cleaned_data['script_type']
        if script_type == '':
            cleaned_data['script_type'] = self.instance.script_type
        else:
            try:
                cleaned_data['script_type'] = translate_script_type(
                    script_type)
            except ValidationError as e:
                set_form_error(self, 'script_type', e)

        hardware_type = cleaned_data['hardware_type']
        if hardware_type == '':
            cleaned_data['hardware_type'] = self.instance.hardware_type
        else:
            try:
                cleaned_data['hardware_type'] = translate_hardware_type(
                    hardware_type)
            except ValidationError as e:
                set_form_error(self, 'hardware_type', e)

        parallel = cleaned_data['parallel']
        if parallel == '':
            cleaned_data['parallel'] = self.instance.parallel
        else:
            try:
                cleaned_data['parallel'] = translate_script_parallel(parallel)
            except ValidationError as e:
                set_form_error(self, 'parallel', e)

        return cleaned_data

    def is_valid(self):
        valid = super().is_valid()

        if valid and self.instance.default and not self.edit_default:
            for field in self.Meta.fields:
                if field in ['tags', 'timeout']:
                    continue
                if field in self.data:
                    set_form_error(
                        self, field,
                        'Not allowed to change on default scripts.')
                    valid = False

        name = self.data.get('name')
        # none is used to tell the API to not run testing_scripts during
        # commissioning.
        if name is not None and name.lower() == 'none':
            set_form_error(self, 'name', '"none" is a reserved name.')
            valid = False

        # The name can't be a digit as MAAS allows scripts to be selected by
        # id.
        if name is not None and name.isdigit():
            set_form_error(self, 'name', 'Cannot be a number.')
            valid = False

        if name is not None and pipes.quote(name) != name:
            set_form_error(
                self, 'name',
                'Name contains disallowed characters, e.g. space or quotes.')
            valid = False

        # If comment and script exist __init__ combines both fields into a dict
        # to pass to VersionedTextFileField.
        if 'comment' in self.data:
            set_form_error(
                self, 'comment',
                '"comment" may only be used when specifying a "script" '
                'as well.')
            valid = False

        if 'script' in self.data:
            if not self._validate_results(self.instance.results):
                valid = False

        if 'parameters' in self.data:
            params_form = ParametersForm(data=self.data.get('parameters'))
            if not params_form.is_valid():
                valid = False

        if (not valid and self.instance.script_id is not None
                and self.initial.get('script') != self.instance.script_id
                and self.instance.script.id is not None):
            # If form validation failed cleanup any new VersionedTextFile
            # created by the VersionedTextFileField.
            self.instance.script.delete()
        return valid

    def save(self, *args, **kwargs):
        request = kwargs.pop('request', None)
        endpoint = kwargs.pop('endpoint', None)
        script = super(ScriptForm, self).save(*args, **kwargs)

        # Create audit event log if endpoint and request supplied.
        if request is not None and endpoint is not None:
            create_audit_event(EVENT_TYPES.SETTINGS,
                               endpoint,
                               request,
                               None,
                               description="Saved script '%s'." % script.name)
        return script
Ejemplo n.º 7
0
 class TestForm(Form):
     test_field = CharField()
Ejemplo n.º 8
0
class UserInfoForm(forms.Form):
    personal_details: CharField = CharField(label="personal_details",
                                            max_length=500)
    money = CharField(label="money", max_length=200)
    letter_of_recommendation: CharField = forms.CharField(label="money",
                                                          max_length=2000)
Ejemplo n.º 9
0
class CreateApplicationForm(forms.Form):
    students: CharField = CharField(label="students", max_length=200)
Ejemplo n.º 10
0
class LoginForm(AuthenticationForm):
    username = CharField(label="Kullanıcı Adı",
                         widget=TextInput(attrs={'class': 'form-control', 'placeholder': 'Kullanıcı adı'}))
    password = CharField(label="Şifre",
                         widget=PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Şifre'}))
Ejemplo n.º 11
0
 class Form(klass):
     entry_description = CharField(label='备注', required=False)
     if not cls.use_today:
         entry_date = DateField(label='日期', initial=date.today())
Ejemplo n.º 12
0
class TextAreaAnswer(BaseAnswerForm):
    answer = CharField(widget=Textarea)
Ejemplo n.º 13
0
class TextInputAnswer(BaseAnswerForm):
    answer = CharField()
Ejemplo n.º 14
0
 def add_hidden_field(self, name, initial):
     self.fields[name] = CharField(widget=HiddenInput,
                                   initial=initial,
                                   required=False)
Ejemplo n.º 15
0
class CoursePwForm(Form):
    password = CharField(label=_('Password'), max_length=255)
Ejemplo n.º 16
0
class LoginForm(Form):
    username = CharField()
    password = CharField(widget=PasswordInput())
    remember_username = BooleanField(initial=True, widget=CheckboxInput())
Ejemplo n.º 17
0
class SubmissionForm(Form):
    problem_slug = CharField(widget=HiddenInput)
    flag = CharField(max_length=255)
Ejemplo n.º 18
0
class SongForm(ModelForm):
    cuex = CharField(widget=LinkWidget, required=False)
    scld = CharField(widget=LinkWidget, required=False)
    midi = CharField(widget=LinkWidget, required=False)
    playback_mp3 = CharField(widget=LinkWidget, required=False)
    recording_ogg = CharField(widget=LinkWidget, required=False)
    recording_mp3 = CharField(widget=LinkWidget, required=False)
    author_songs = CharField(widget=DirectLinkWidget, required=False)
    song_Embed_Description = CharField(widget=DirectLinkWidget, required=False)
    user_info = CharField(widget=DirectLinkWidget, required=False)
    song_counters = CharField(widget=DirectLinkWidget, required=False)

    def __init__(self, *args, **kwargs):
        super(SongForm, self).__init__(*args, **kwargs)
        self.set_initial_values()

    def set_initial_values(self):
        api_base_url = "https://my.scorecloud.com/api/2.0/song/"

        if self.instance.source_type == "recording":
            self.fields[
                'recording_ogg'].initial = api_base_url + self.instance.song_id + "/recording.ogg"
            self.fields[
                'recording_mp3'].initial = api_base_url + self.instance.song_id + "/recording.mp3"

        self.fields[
            'cuex'].initial = api_base_url + self.instance.song_id + "/cuex"
        self.fields[
            'scld'].initial = api_base_url + self.instance.song_id + "/song.scld"
        self.fields[
            'midi'].initial = api_base_url + self.instance.song_id + "/playback.midi"
        self.fields[
            'playback_mp3'].initial = api_base_url + self.instance.song_id + "/playback.mp3"
        self.fields['author_songs'].initial = get_song_author_link(
            self.instance.author)
        self.fields['song_Embed_Description'].initial = get_song_meta_link(
            self.instance)
        self.fields['user_info'].initial = get_user_info_link(
            self.instance.author)
        self.fields['song_counters'].initial = get_song_counters_link(
            self.instance)

    class Meta:
        model = Song
        fields = (
            'song_id',
            'source_type',
            'title',
            'creation_date',
            'is_active',
            'is_shared',
            'is_deleted',
            'song_Embed_Description',
            'is_unsorted',
            'current_scld_id',
            'meta',
            'current_revision_id',
            'last_update',
            'permissions',
            'is_public',
            'popularity',
            'maturity',
        )
Ejemplo n.º 19
0
class RequestForm(ModelForm):
    cstatus =  CharField(label='Status', required=False)

    class Meta:
        model = TRequest
Ejemplo n.º 20
0
class ChoiceFieldExclusionForm(ModelForm):
    multi_choice = CharField(max_length=50)

    class Meta:
        exclude = ['multi_choice']
        model = ChoiceFieldModel
Ejemplo n.º 21
0
class CreateUserForm(Form):
    appuseruuid = CharField(widget=HiddenInput())
    email = CharField(widget=EmailInput(attrs={'class': 'form-control'}))
    username = CharField(widget=TextInput(attrs={'class': 'form-control'}))
    first_name = CharField(widget=TextInput(attrs={'class': 'form-control'}))
    last_name = CharField(widget=TextInput(attrs={'class': 'form-control'}))
    password = CharField(widget=PasswordInput(attrs={'class': 'form-control'}))
    confirm_password = CharField(widget=PasswordInput(
        attrs={'class': 'form-control'}))
    user_agreement_consent = BooleanField(label=mark_safe(
        'I have read and agree to the <a href="/end-user-license/">user license agreement</a>'
    ),
                                          widget=CheckboxInput())
    privacy_policy_consent = BooleanField(label=mark_safe(
        'I have read and agree to the <a href="/privacy-policy/">privacy policy</a>'
    ),
                                          widget=CheckboxInput())

    def __init__(self, registration_type='website', *args, **kwargs):
        super(CreateUserForm, self).__init__(*args, **kwargs)
        # self.fields['username'].help_text = 'Minimum 3 characters. Letters, numbers, and underscores only.'
        self.fields[
            'password'].help_text = 'Minimum 8 characters. Must contain at least 1 letter, 1 number and 1 special character.'
        self.registration_type = registration_type
        if registration_type == 'invitation':
            self.fields['email'].widget = EmailInput(
                attrs={'class': 'form-control'})
        else:
            self.fields['email'].widget = HiddenInput()
            self.fields['user_agreement_consent'].widget = HiddenInput(
                attrs={'value': 'checked'})
            self.fields['privacy_policy_consent'].widget = HiddenInput(
                attrs={'value': 'checked'})

        if settings.MAKE_USERNAME_EMAIL:
            self.fields['username'].widget = HiddenInput()
            self.fields['username'].required = False
        else:
            self.fields['username'].widget = TextInput(
                attrs={'class': 'form-control'})

    def clean(self):
        """ Enforces username and password requirements
        """
        super(CreateUserForm, self).clean()
        data = self.cleaned_data
        errors = []

        clean_password = self.cleaned_data['password']
        confirm_password = self.cleaned_data['confirm_password']
        password_errors = validate_password(clean_password, confirm_password)
        if password_errors:
            errors.append(password_errors)

        clean_email = self.cleaned_data['email']
        email_error = validate_email(clean_email, 0)
        if email_error:
            errors.append(email_error)
        if self.registration_type == 'invitation':
            email_is_unique = validate_unique_email(clean_email, 0)
        if not settings.MAKE_USERNAME_EMAIL:
            username_is_unique = validate_unique_username(
                self.cleaned_data['username'], 0)
        if errors:
            raise ValidationError(mark_safe('<br />'.join(errors)))
        return data
Ejemplo n.º 22
0
class ProjectForm(forms.Form):
    project_slug = CharField(label='App Name',
                             max_length=100,
                             widget=TextInput(attrs={
                                 'readonly': 'readonly',
                                 'class': 'form-control'
                             }))
    organisation_name = CharField(
        label='Name der Genossenschaft',
        max_length=100,
        widget=TextInput(attrs={'class': 'form-control'}))
    street = CharField(label='Strasse',
                       max_length=100,
                       widget=TextInput(attrs={'class': 'form-control'}))
    number = CharField(label='Nummer',
                       max_length=100,
                       widget=TextInput(attrs={'class': 'form-control'}))
    zip = CharField(label='PLZ',
                    max_length=100,
                    widget=TextInput(attrs={'class': 'form-control'}))
    city = CharField(label='Stadt',
                     max_length=100,
                     widget=TextInput(attrs={'class': 'form-control'}))
    extra = CharField(label='AdressZusatz',
                      max_length=100,
                      widget=TextInput(attrs={'class': 'form-control'}),
                      required=False)
    PC = CharField(label='PC Kontonummer',
                   max_length=100,
                   widget=TextInput(attrs={'class': 'form-control'}))
    IBAN = CharField(label='IBAN',
                     max_length=100,
                     widget=TextInput(attrs={'class': 'form-control'}))
    BIC = CharField(label='BIC',
                    max_length=100,
                    widget=TextInput(attrs={'class': 'form-control'}))
    NAME = CharField(label='Name der Bank',
                     max_length=100,
                     widget=TextInput(attrs={'class': 'form-control'}))
    ESR = CharField(label='ESR falls vorhanden',
                    max_length=100,
                    widget=TextInput(attrs={'class': 'form-control'}),
                    required=False)
    info_email = CharField(label='info email adresse',
                           max_length=100,
                           widget=TextInput(attrs={'class': 'form-control'}))
    share_price = CharField(label='Preis eines Anteilscheines',
                            max_length=100,
                            widget=TextInput(attrs={'class': 'form-control'}))
Ejemplo n.º 23
0
class UserProfileEmailUsernameForm(UserProfileForm):
    user_id = CharField(widget=HiddenInput())
    email = EmailField(widget=EmailInput(attrs={'class': 'form-control'}))
    first_name = CharField(widget=TextInput(attrs={'class': 'form-control'}))
    last_name = CharField(widget=TextInput(attrs={'class': 'form-control'}))
Ejemplo n.º 24
0
class DomainForm(forms.Form):
    domain = CharField(label='Domain',
                       max_length=100,
                       widget=TextInput(attrs={'class': 'form-control'}))
Ejemplo n.º 25
0
class NameForm(Form):
    your_name = CharField(label='Your name', max_length=100)
Ejemplo n.º 26
0
 def test_disabled_has_changed(self):
     f = MultiValueField(fields=(CharField(), CharField()), disabled=True)
     self.assertIs(f.has_changed(['x', 'x'], ['y', 'y']), False)
Ejemplo n.º 27
0
 def test_charfield_disabled(self):
     f = CharField(disabled=True)
     self.assertWidgetRendersTo(
         f, '<input type="text" name="f" id="id_f" disabled required>')
Ejemplo n.º 28
0
class ForgotUsernameForm(Form):
    email = CharField(label="Email address", max_length=256)
Ejemplo n.º 29
0
        class ExcludingForm(ModelForm):
            name = CharField(max_length=255)

            class Meta:
                model = Defaults
                exclude = ['name', 'callable_default']
Ejemplo n.º 30
0
 class MyForm(Form):
     field1 = CharField(max_length=20, widget=MyWidget1())
     field2 = CharField(max_length=20, widget=MyWidget2())