def on_bind(self) -> None: bind_members(self, name='filters') self.advanced_simple_toggle = self.advanced_simple_toggle.bind(parent=self) request = self.get_request() self.query_advanced_value = request_data(request).get(self.get_advanced_query_param(), '') if request else '' # TODO: should it be possible to have freetext as a callable? this code just treats callables as truthy if any(f.freetext for f in values(declared_members(self)['filters'])): declared_members(self.form).fields[FREETEXT_SEARCH_NAME].include = True declared_fields = declared_members(self.form)['fields'] for name, filter in items(self.filters): assert filter.attr or not getattr(filter.value_to_q, 'iommi_needs_attr', False), f"{name} cannot be a part of a query, it has no attr or value_to_q so we don't know what to search for" if name in declared_fields: field = setdefaults_path( Namespace(), _name=name, attr=name if filter.attr is MISSING else filter.attr, model_field=filter.model_field, ) declared_fields[name] = declared_fields[name].reinvoke(field) set_declared_member(self.form, 'fields', declared_fields) for name, field in items(declared_fields): if name == FREETEXT_SEARCH_NAME: continue if name not in self.filters: field.include = False bind_members(self, name='endpoints') self.form = self.form.bind(parent=self) self._bound_members.form = self.form
def collect_members(container, *, name: str, items_dict: Dict = None, items: Dict[str, Any] = None, cls: Type, unknown_types_fall_through=False): forbidden_names = FORBIDDEN_NAMES & (set(keys(items_dict or {})) | set(keys(items or {}))) if forbidden_names: raise ForbiddenNamesException(f'The names {", ".join(sorted(forbidden_names))} are reserved by iommi, please pick other names') assert name != 'items' unbound_items = Struct() _unapplied_config = {} if items_dict is not None: for key, x in items_of(items_dict): x._name = key unbound_items[key] = x if items is not None: for key, item in items_of(items): if isinstance(item, Traversable): # noinspection PyProtectedMember assert not item._is_bound item._name = key unbound_items[key] = item elif isinstance(item, dict): if key in unbound_items: _unapplied_config[key] = item else: item = setdefaults_path( Namespace(), item, call_target__cls=cls, _name=key, ) unbound_items[key] = item() else: assert unknown_types_fall_through or item is None, f'I got {type(item)} when creating a {cls.__name__}.{key}, but I was expecting Traversable or dict' unbound_items[key] = item for k, v in items_of(Namespace(_unapplied_config)): unbound_items[k] = unbound_items[k].reinvoke(v) # noinspection PyProtectedMember assert unbound_items[k]._name is not None to_delete = { k for k, v in items_of(unbound_items) if v is None } for k in to_delete: del unbound_items[k] sort_after(unbound_items) set_declared_member(container, name, unbound_items) setattr(container, name, NotBoundYet(container, name))
def on_bind(self) -> None: bind_members(self, name='filters') request = self.get_request() self.query_advanced_value = request_data(request).get( self.get_advanced_query_param(), '') if request else '' # TODO: should it be possible to have freetext as a callable? this code just treats callables as truthy if any(f.freetext for f in values(declared_members(self)['filters'])): set_and_remember_for_reinvoke(declared_members( self.form).fields[FREETEXT_SEARCH_NAME], include=True) declared_fields = declared_members(self.form)['fields'] for name, filter in items(self.filters): is_valid, message = filter.is_valid_filter(name=name, filter=filter) assert is_valid, message if name in declared_fields: field = setdefaults_path( Namespace(), _name=name, attr=name if filter.attr is MISSING else filter.attr, model_field=filter.model_field, help__include=False, ) declared_fields[name] = reinvoke(declared_fields[name], field) set_declared_member(self.form, 'fields', declared_fields) # Remove fields from the form that correspond to non-included filters declared_filters = declared_members(self)['filters'] for name, field in items(declared_fields): if name == FREETEXT_SEARCH_NAME: continue # We need to check if it's in declared_filters first, otherwise we remove any injected fields if name in declared_filters and name not in self.filters: set_and_remember_for_reinvoke(field, include=False) bind_members(self, name='endpoints') self.form = self.form.bind(parent=self) self._bound_members.form = self.form self.advanced = self._bound_members.advanced = self.advanced.bind( parent=self) self.form_container = self.form_container.bind(parent=self) self.filter_name_by_query_name = { x.query_name: name for name, x in items(self.filters) }
def test_warning_when_names_are_recalculated(capsys): page = Page(parts__foo=Fragment(_name='foo')) assert get_path_by_long_path(page) == {'parts/foo': ''} out, err = capsys.readouterr() assert out == '' set_declared_member(page, 'bar', Fragment(_name='bar')) assert get_path_by_long_path(page) == { 'parts/foo': '', 'bar': 'bar', } out, err = capsys.readouterr() assert out == '### A disturbance in the force... The namespace has been recalculated!\n'
def __init__(self, *, model=None, rows=None, filters=None, _filters_dict=None, auto, **kwargs): assert isinstance(filters, dict) if auto: auto = QueryAutoConfig(**auto) auto_model, auto_rows, filters = self._from_model( model=auto.model, rows=auto.rows, filters=filters, include=auto.include, exclude=auto.exclude, ) assert model is None, "You can't use the auto feature and explicitly pass model. " \ "Either pass auto__model, or we will set the model for you from auto__rows" model = auto_model if rows is None: rows = auto_rows model, rows = model_and_rows(model, rows) setdefaults_path( kwargs, form__call_target=self.get_meta().form_class, ) self._form = None self.query_advanced_value = None self.query_error = None super(Query, self).__init__(model=model, rows=rows, **kwargs) collect_members(self, name='filters', items=filters, items_dict=_filters_dict, cls=self.get_meta().member_class) field_class = self.get_meta().form_class.get_meta().member_class declared_fields = Struct() declared_fields[FREETEXT_SEARCH_NAME] = field_class( _name=FREETEXT_SEARCH_NAME, display_name='Search', required=False, include=False, ) for name, filter in items(declared_members(self).filters): if filter.attr is None and getattr(filter.value_to_q, 'iommi_needs_attr', False): continue field = setdefaults_path( Namespace(), filter.field, _name=name, model_field=filter.model_field, attr=name if filter.attr is MISSING else filter.attr, call_target__cls=field_class, ) declared_fields[name] = field() # noinspection PyCallingNonCallable self.form: Form = self.form( _name='form', _fields_dict=declared_fields, attrs__method='get', actions__submit__attrs__value='Filter', ) declared_members(self).form = self.form self.advanced_simple_toggle = Action( attrs__href='#', attrs__class__iommi_query_toggle_simple_mode=True, attrs={'data-advanced-mode': 'simple'}, display_name='Switch to advanced search', ) self.form_container = self.form_container() # Filters need to be at the end to not steal the short names set_declared_member(self, 'filters', declared_members(self).pop('filters'))
def __init__(self, *, model=None, rows=None, filters=None, _filters_dict=None, auto=None, **kwargs): assert isinstance(filters, dict) if auto: auto = QueryAutoConfig(**auto) auto_model, auto_rows, filters = self._from_model( model=auto.model, rows=auto.rows, filters=filters, include=auto.include, exclude=auto.exclude, ) assert model is None, "You can't use the auto feature and explicitly pass model. " \ "Either pass auto__model, or we will set the model for you from auto__rows" model = auto_model if rows is None: rows = auto_rows model, rows = model_and_rows(model, rows) setdefaults_path( kwargs, form__call_target=self.get_meta().form_class, ) self.query_advanced_value = None self.query_error = None # Here we need to remove the freetext config from kwargs because we want to # handle it differently from the other fields. # BUT It's not safe to modify kwargs deeply! Because reinvoke() is evil: # It will call init again with the same kwargs + whatever additional kwargs # might have come from a parent or styling info. freetext_config = kwargs.get('form', {}).get('fields', {}).get('freetext', {}) if 'form' in kwargs and 'fields' in kwargs[ 'form'] and 'freetext' in kwargs['form']['fields']: # copy (just) the namespace so we can safely remove freetext from it kwargs = Namespace(flatten(Namespace(kwargs))) freetext_config = kwargs.get('form', {}).get('fields', {}).pop('freetext', {}) super(Query, self).__init__(model=model, rows=rows, **kwargs) collect_members(self, name='filters', items=filters, items_dict=_filters_dict, cls=self.get_meta().member_class) field_class = self.get_meta().form_class.get_meta().member_class declared_fields = Struct() declared_fields[FREETEXT_SEARCH_NAME] = field_class( _name=FREETEXT_SEARCH_NAME, display_name=gettext('Search'), required=False, include=False, help__include=False, **freetext_config) for name, filter in items(declared_members(self).filters): field = setdefaults_path( Namespace(), filter.field, _name=name, model_field=filter.model_field, attr=name if filter.attr is MISSING else filter.attr, call_target__cls=field_class, help__include=False, ) declared_fields[name] = field() # noinspection PyCallingNonCallable self.form: Form = self.form( _name='form', _fields_dict=declared_fields, attrs__method='get', actions__submit=dict( attrs={'data-iommi-filter-button': ''}, display_name=gettext('Filter'), ), ) declared_members(self).form = self.form self.advanced = declared_members(self).advanced = self.advanced( _name='advanced') self.form_container = self.form_container(_name='form_container') # Filters need to be at the end to not steal the short names set_declared_member(self, 'filters', declared_members(self).pop('filters'))
def collect_members( container, *, name: str, items_dict: Dict = None, items: Dict[str, Any] = None, cls: Type, unknown_types_fall_through=False, ): """ This function is used to collect and merge data from the constructor argument, the declared members, and other config into one data structure. `bind_members` is then used at bind time to recursively bind the nested parts. Example: .. code:: python class ArtistTable(Table): instrument = Column() # <- declared member MyTable( columns__name=Column(), # <- constructor argument columns__instrument__after='name', # <- inserted config for a declared member ) In this example the resulting table will have two columns `instrument` and `name`, with `instrument` after name even though it was declared before. """ forbidden_names = FORBIDDEN_NAMES & (set(keys(items_dict or {})) | set(keys(items or {}))) if forbidden_names: raise ForbiddenNamesException( f'The names {", ".join(sorted(forbidden_names))} are reserved by iommi, please pick other names' ) assert name != 'items' unbound_items = Struct() _unapplied_config = {} if items_dict is not None: for key, x in items_of(items_dict): x._name = key unbound_items[key] = x if items is not None: for key, item in items_of(items): if isinstance(item, Traversable): # noinspection PyProtectedMember assert not item._is_bound item._name = key unbound_items[key] = item elif isinstance(item, dict): if key in unbound_items: _unapplied_config[key] = item else: item = setdefaults_path( Namespace(), item, call_target__cls=cls, _name=key, ) unbound_items[key] = item() else: assert ( unknown_types_fall_through or item is None ), f'I got {type(item)} when creating a {cls.__name__}.{key}, but I was expecting Traversable or dict' unbound_items[key] = item for k, v in items_of(Namespace(_unapplied_config)): unbound_items[k] = reinvoke(unbound_items[k], v) # noinspection PyProtectedMember assert unbound_items[k]._name is not None to_delete = {k for k, v in items_of(unbound_items) if v is None} for k in to_delete: del unbound_items[k] sort_after(unbound_items) set_declared_member(container, name, unbound_items) setattr(container, name, NotBoundYet(container, name))