Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
list.py202 linesDownload Raw Back to fields
1import itertools2 3from .. import widgets4from .core import Field5from .core import UnboundField6from wtforms.utils import unset_value7 8__all__ = ("FieldList",)9 10 11class FieldList(Field):12    """13    Encapsulate an ordered list of multiple instances of the same field type,14    keeping data as a list.15 16    >>> authors = FieldList(StringField('Name', [validators.DataRequired()]))17 18    :param unbound_field:19        A partially-instantiated field definition, just like that would be20        defined on a form directly.21    :param min_entries:22        if provided, always have at least this many entries on the field,23        creating blank ones if the provided input does not specify a sufficient24        amount.25    :param max_entries:26        accept no more than this many entries as input, even if more exist in27        formdata.28    :param separator:29        A string which will be suffixed to this field's name to create the30        prefix to enclosed list entries. The default is fine for most uses.31    """32 33    widget = widgets.ListWidget()34 35    def __init__(36        self,37        unbound_field,38        label=None,39        validators=None,40        min_entries=0,41        max_entries=None,42        separator="-",43        default=(),44        **kwargs,45    ):46        super().__init__(label, validators, default=default, **kwargs)47        if self.filters:48            raise TypeError(49                "FieldList does not accept any filters. Instead, define"50                " them on the enclosed field."51            )52        assert isinstance(53            unbound_field, UnboundField54        ), "Field must be unbound, not a field class"55        self.unbound_field = unbound_field56        self.min_entries = min_entries57        self.max_entries = max_entries58        self.last_index = -159        self._prefix = kwargs.get("_prefix", "")60        self._separator = separator61        self._field_separator = unbound_field.kwargs.get("separator", "-")62 63    def process(self, formdata, data=unset_value, extra_filters=None):64        if extra_filters:65            raise TypeError(66                "FieldList does not accept any filters. Instead, define"67                " them on the enclosed field."68            )69 70        self.entries = []71        if data is unset_value or not data:72            try:73                data = self.default()74            except TypeError:75                data = self.default76 77        self.object_data = data78 79        if formdata:80            indices = sorted(set(self._extract_indices(self.name, formdata)))81            if self.max_entries:82                indices = indices[: self.max_entries]83 84            idata = iter(data)85            for index in indices:86                try:87                    obj_data = next(idata)88                except StopIteration:89                    obj_data = unset_value90                self._add_entry(formdata, obj_data, index=index)91        else:92            for obj_data in data:93                self._add_entry(formdata, obj_data)94 95        while len(self.entries) < self.min_entries:96            self._add_entry(formdata)97 98    def _extract_indices(self, prefix, formdata):99        """100        Yield indices of any keys with given prefix.101 102        formdata must be an object which will produce keys when iterated.  For103        example, if field 'foo' contains keys 'foo-0-bar', 'foo-1-baz', then104        the numbers 0 and 1 will be yielded, but not necessarily in order.105        """106        offset = len(prefix) + 1107        for k in formdata:108            if k.startswith(prefix):109                k = k[offset:].split(self._field_separator, 1)[0]110                if k.isdigit():111                    yield int(k)112 113    def validate(self, form, extra_validators=()):114        """115        Validate this FieldList.116 117        Note that FieldList validation differs from normal field validation in118        that FieldList validates all its enclosed fields first before running any119        of its own validators.120        """121        self.errors = []122 123        # Run validators on all entries within124        for subfield in self.entries:125            subfield.validate(form)126            self.errors.append(subfield.errors)127 128        if not any(x for x in self.errors):129            self.errors = []130 131        chain = itertools.chain(self.validators, extra_validators)132        self._run_validation_chain(form, chain)133 134        return len(self.errors) == 0135 136    def populate_obj(self, obj, name):137        values = getattr(obj, name, None)138        try:139            ivalues = iter(values)140        except TypeError:141            ivalues = iter([])142 143        candidates = itertools.chain(ivalues, itertools.repeat(None))144        _fake = type("_fake", (object,), {})145        output = []146        for field, data in zip(self.entries, candidates):147            fake_obj = _fake()148            fake_obj.data = data149            field.populate_obj(fake_obj, "data")150            output.append(fake_obj.data)151 152        setattr(obj, name, output)153 154    def _add_entry(self, formdata=None, data=unset_value, index=None):155        assert (156            not self.max_entries or len(self.entries) < self.max_entries157        ), "You cannot have more than max_entries entries in this FieldList"158        if index is None:159            index = self.last_index + 1160        self.last_index = index161        name = f"{self.short_name}{self._separator}{index}"162        id = f"{self.id}{self._separator}{index}"163        field = self.unbound_field.bind(164            form=None,165            name=name,166            prefix=self._prefix,167            id=id,168            _meta=self.meta,169            translations=self._translations,170        )171        field.process(formdata, data)172        self.entries.append(field)173        return field174 175    def append_entry(self, data=unset_value):176        """177        Create a new entry with optional default data.178 179        Entries added in this way will *not* receive formdata however, and can180        only receive object data.181        """182        return self._add_entry(data=data)183 184    def pop_entry(self):185        """Removes the last entry from the list and returns it."""186        entry = self.entries.pop()187        self.last_index -= 1188        return entry189 190    def __iter__(self):191        return iter(self.entries)192 193    def __len__(self):194        return len(self.entries)195 196    def __getitem__(self, index):197        return self.entries[index]198 199    @property200    def data(self):201        return [f.data for f in self.entries]202 
codekingpro/portable-devtools · Team Ai