def form_example_4(request): ensure_objects() return Form.as_edit_page(instance=Foo.objects.all().first(), actions=dict( foo=Action.submit(attrs__value='Foo'), bar=Action.submit(attrs__value='Bar'), back=Action(display_name='Back to index', attrs__href='/'), ))
def form_example_6(request): return Form.edit(auto__instance=Artist.objects.all().first(), actions=dict( foo=Action.submit(attrs__value='Foo'), bar=Action.submit(attrs__value='Bar'), a=Action.submit(attrs__value='Foo', group='x'), b=Action.submit(attrs__value='Bar', group='x'), back=Action(display_name='Back to index', attrs__href='/'), ))
def form_example_5(request): ensure_objects() return Form.as_create_page( model=Bar, fields__b__input__template='iommi/form/choice_select2.html', actions=dict( foo=Action.submit(attrs__value='Foo'), bar=Action.submit(attrs__value='Bar'), back=Action(display_name='Back to index', attrs__href='/'), ))
def results(request, question_id): # question = get_object_or_404(Question, pk=question_id) # return render(request, 'polls/results.html', {'question': question}) # OR # class ResultsView(generic.DetailView): # model = Question # template_name = 'polls/results.html' # <h1>{{ question.question_text }}</h1> # # <ul> # {% for choice in question.choice_set.all %} # <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> # {% endfor %} # </ul> # # <a href="{% url 'polls:detail' question.id %}">Vote again?</a> question = get_object_or_404(Question, pk=question_id) return Table( title=str(question), auto__rows=question.choice_set.all(), auto__exclude=['question'], actions__vote_again=Action(display_name='Vote again?', attrs__href=reverse('polls:vote', args=(question_id, ))), actions_below=True, )
def test_render(): action = Action( _name='do_it', attrs__href='#' ).bind() assert_renders(action, ''' <a href="#"> Do it </a> ''')
def test_render_class(): action = Action( _name='do_it', attrs__class__foo=True ).bind() assert_renders(action, ''' <a class="foo">Do it</a> ''')
def test_render_icon(): submit = Action.icon( icon='flower', display_name='Name', ).bind() assert_renders(submit, ''' <a> <i class="fa fa-flower"> </i> Name </a> ''')
def test_render_submit(): submit = Action.submit(display_name='Do it').bind() assert_renders( submit, ''' <button accesskey="s" name="-">Do it</button> ''', )
def test_render_button(): submit = Action.button(_name='do_it').bind() assert_renders( submit, ''' <button> Do it </button> ''', )
def test_render_input(): action = Action(_name='do_it', tag='input', attrs__href='#').bind() assert_renders( action, ''' <input href="#" value="Do it"> ''', )
class IndexPage(ExamplesPage): header = html.h1('Form examples') description = html.p('Some examples of iommi Forms') examples = example_links(examples) all_fields = Action( display_name='Example with all types of fields', attrs__href='all_fields', )
class Meta: sortable = False actions = dict( a=Action(display_name='Foo', attrs__href='/foo/', include=lambda table, **_: table.rows is not rows), b=Action(display_name='Bar', attrs__href='/bar/', include=lambda table, **_: table.rows is rows), c=Action(display_name='Baz', attrs__href='/bar/', group='Other'), d=dict(display_name='Qux', attrs__href='/bar/', group='Other'), e=Action.icon('icon_foo', display_name='Icon foo', attrs__href='/icon_foo/'), f=Action.icon('icon_bar', icon_classes=['lg'], display_name='Icon bar', attrs__href='/icon_bar/'), g=Action.icon('icon_baz', icon_classes=['one', 'two'], display_name='Icon baz', attrs__href='/icon_baz/'), )
def example_links(examples): children = {} for i, example in enumerate(examples): n = i + 1 children[f'example_{n}'] = html.p( Action( display_name=f'Example {n}: {example.description}', attrs__href=f'example_{n}', ), html.br(), ) return Fragment(children=children)
def example_links(examples): result = {} for i, example in enumerate(examples): n = i + 1 result[f'example_{n}'] = html.p( Action( display_name=f'Example {n}: {example.description}', attrs__href=f'example_{n}', ), html.br(), ) return result
class IndexPage(ExamplesPage): header = html.h1('Form examples') description = html.p('Some examples of iommi Forms') all_fields = html.p( Action( display_name='Example with all types of fields', attrs__href='all_fields', ), html.br(), after='example_11', ) class Meta: parts = example_links(examples)
def test_action_groups(): non_grouped, grouped = group_actions(dict( a=Action(), b=Action(), c=Action(group='a'), d=Action(group='a'), e=Action(group='a'), f=Action(group='b'), g=Action(group='b'), )) assert len(non_grouped) == 2 assert len(grouped) == 2 assert len(grouped[0][2]) == 3 assert len(grouped[1][2]) == 2
class Meta: auto__model = Album page_size = 20 columns__name__cell__url = lambda row, **_: row.get_absolute_url() columns__name__filter__include = True columns__year__filter__include = True columns__year__filter__field__include = False columns__artist__filter__include = True columns__edit = Column.edit( include=lambda request, **_: request.user.is_staff, ) columns__delete = Column.delete( include=lambda request, **_: request.user.is_staff, ) actions__create_album = Action(attrs__href='/albums/create/', display_name=_('Create album'))
class IndexPage(ExamplesPage): header = html.h1('Table examples') description = html.p('Some examples of iommi tables') all_fields = html.p( Action( display_name='Example with all available types of columns', attrs__href='all_columns', ), html.br(), after='example_6', ) class Meta: parts = example_links(examples)
def index(request, path=''): p = (Path(settings.ARCHIVE_PATH) / path).absolute() assert str(p).startswith(str(Path(settings.ARCHIVE_PATH).absolute())), 'Someone is trying to hack the site' if p.is_file(): return FileResponse(open(p, 'rb'), filename=p.name) rows = [ x for x in p.glob('*') if not x.name.startswith('.') ] if request.GET.get('gallery') is not None: images = [ x for x in rows if is_image(x) ] return Table.div( rows=sorted(images, key=lambda x: x.name.lower()), page_size=None, columns__name=Column(cell__format=lambda row, **_: mark_safe(f'<img src="{row.name}" style="max-width: 100%">')) ) return Table( columns=dict( icon=Column( display_name='', header__attrs__style__width='25px', attr=None, cell__format=lambda row, **_: mark_safe('<i class="far fa-folder"></i>') if row.is_dir() else '' ), name=Column( cell__url=lambda row, **_: f'{row.name}/' if row.is_dir() else row.name, ), ), page_size=None, rows=sorted(rows, key=lambda x: x.name.lower()), actions__gallery=Action(attrs__href='?gallery') )
def test_display_name_to_value_attr_but_attr_overrides(): assert Action.delete( display_name='foo', attrs__value='bar').bind(request=None).__html__( ) == '<input accesskey="s" name="-" type="submit" value="bar">'
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'))
class Query(Part): """ Declare a query language. Example: .. code:: python class CarQuery(Query): make = Filter.choice(choices=['Toyota', 'Volvo', 'Ford']) model = Filter() query_set = Car.objects.filter( CarQuery().bind(request=request).get_q() ) """ form: Namespace = Refinable() model: Type[Model] = Refinable( ) # model is evaluated, but in a special way so gets no EvaluatedRefinable type rows = Refinable() template: Union[str, Template] = EvaluatedRefinable() form_container: Fragment = EvaluatedRefinable() member_class = Refinable() form_class = Refinable() class Meta: member_class = Filter form_class = Form @reinvokable @dispatch( endpoints__errors__func=default_endpoint__errors, filters=EMPTY, auto=EMPTY, form__attrs={ 'data-iommi-errors': lambda query, **_: query.endpoints.errors.iommi_path }, form_container__call_target=Fragment, form_container__tag='span', form_container__attrs__class__iommi_query_form_simple=True, ) 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')) @dispatch( render__call_target=render_template, ) def __html__(self, *, render=None): if not self.iommi_bound_members().filters._bound_members: return '' setdefaults_path( render, context=self.iommi_evaluate_parameters(), template=self.template, ) return render(request=self.get_request()) 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, help__include=False, ) 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 self.form_container = self.form_container.bind(parent=self) def own_evaluate_parameters(self): return dict(query=self) def get_advanced_query_param(self): return '-' + path_join(self.iommi_path, 'query') def parse_query_string(self, query_string: str) -> Q: assert self._is_bound query_string = query_string.strip() if not query_string: return Q() parser = self._create_grammar() try: tokens = parser.parseString(query_string, parseAll=True) except ParseException as e: raise QueryException('Invalid syntax for query') return self._compile(tokens) def _compile(self, tokens) -> Q: items = [] for token in tokens: if isinstance(token, ParseResults): items.append(self._compile(token)) elif isinstance(token, Q): items.append(token) elif token in ('and', 'or'): items.append(token) return self._rpn_to_q(self._tokens_to_rpn(items)) @staticmethod def _rpn_to_q(tokens): stack = [] for each in tokens: if isinstance(each, Q): stack.append(each) else: op = each # infix right hand operator is on the top of the stack right, left = stack.pop(), stack.pop() stack.append(left & right if op == 'and' else left | right) assert len(stack) == 1 return stack[0] @staticmethod def _tokens_to_rpn(tokens): # Convert a infix sequence of Q objects and 'and/or' operators using # dijkstra shunting yard algorithm into RPN if len(tokens) == 1: return tokens result_q, stack = [], [] for token in tokens: assert token is not None if isinstance(token, Q): result_q.append(token) elif token in PRECEDENCE: p1 = PRECEDENCE[token] while stack: t2, p2 = stack[-1] if p1 <= p2: stack.pop() result_q.append(t2) else: # pragma: no cover break # pragma: no mutate stack.append((token, PRECEDENCE[token])) while stack: result_q.append(stack.pop()[0]) return result_q def _create_grammar(self): """ Pyparsing implementation of a where clause grammar based on http://pyparsing.wikispaces.com/file/view/simpleSQL.py The query language is a series of statements separated by AND or OR operators and parentheses can be used to group/provide precedence. A statement is a combination of three strings "<filter> <operator> <value>" or "<filter> <operator> <filter>". A value can be a string, integer or a real(floating) number or a (ISO YYYY-MM-DD) date. An operator must be one of "= != < > >= <= !:" and are translated into django __lte or equivalent suffixes. See self.as_q Example something < 10 AND other >= 2015-01-01 AND (foo < 1 OR bar > 1) """ quoted_string_excluding_quotes = QuotedString( '"', escChar='\\').setParseAction(lambda token: StringValue(token[0])) and_ = Keyword('and', caseless=True) or_ = Keyword('or', caseless=True) binary_op = oneOf('=> =< = < > >= <= : != !:', caseless=True).setResultsName('operator') # define query tokens identifier = Word(alphas, alphanums + '_$-.').setName('identifier') raw_value_chars = alphanums + '_$-+/$%*;?@[]\\^`{}|~.' raw_value = Word(raw_value_chars, raw_value_chars).setName('raw_value') value_string = quoted_string_excluding_quotes | raw_value # Define a where expression where_expression = Forward() binary_operator_statement = (identifier + binary_op + value_string).setParseAction( self._binary_op_to_q) unary_operator_statement = (identifier | (Char('!') + identifier)).setParseAction( self._unary_op_to_q) free_text_statement = quotedString.copy().setParseAction( self._freetext_to_q) operator_statement = binary_operator_statement | free_text_statement | unary_operator_statement where_condition = Group(operator_statement | ('(' + where_expression + ')')) where_expression << where_condition + ZeroOrMore( (and_ | or_) + where_expression) # define the full grammar query_statement = Forward() query_statement << Group(where_expression).setResultsName("where") return query_statement def _unary_op_to_q(self, token): if len(token) == 1: (filter_name, ) = token value = 'true' else: (op, filter_name) = token value = 'false' if op != '!': # pragma: no cover. You can't actually get here because you'll get a syntax error earlier raise QueryException( f'Unknown unary filter operator "{op}", available operators: !' ) filter = self.filters.get(filter_name.lower()) if filter: if not filter.unary: raise QueryException( f'"{filter_name}" is not a unary filter, you must use it like "{filter_name}=something"' ) result = filter.value_to_q(filter=filter, op='=', value_string_or_f=value) return result raise QueryException( f'Unknown unary filter "{filter_name}", available filters: {", ".join(list(keys(self.filters)))}' ) def _binary_op_to_q(self, token): """ Convert a parsed token of filter_name OPERATOR filter_name into a Q object """ assert self._is_bound filter_name, op, value_string_or_filter_name = token if filter_name.endswith('.pk'): filter = self.filters.get(filter_name.lower()[:-len('.pk')]) if op != '=': raise QueryException( 'Only = is supported for primary key lookup') try: pk = int(value_string_or_filter_name) except ValueError: raise QueryException( f'Could not interpret {value_string_or_filter_name} as an integer' ) return Q(**{f'{filter.attr}__pk': pk}) filter = self.filters.get(filter_name.lower()) if filter: if isinstance(value_string_or_filter_name, str) and not isinstance( value_string_or_filter_name, StringValue ) and value_string_or_filter_name.lower() in self.filters: value_string_or_f = F( self.filters[value_string_or_filter_name.lower()].attr) else: value_string_or_f = value_string_or_filter_name try: result = filter.value_to_q(filter=filter, op=op, value_string_or_f=value_string_or_f) except ValidationError as e: raise QueryException(f'{e.message}') if result is None: raise QueryException( f'Unknown value "{value_string_or_f}" for filter "{filter._name}"' ) return result raise QueryException( f'Unknown filter "{filter_name}", available filters: {list(keys(self.filters))}' ) def _freetext_to_q(self, token): if all(not v.freetext for v in values(self.filters)): raise QueryException('There are no freetext filters available') assert len(token) == 1 token = token[0].strip('"') return reduce(operator.or_, [ Q( **{ filter.attr + '__' + filter.query_operator_to_q_operator(':'): token }) for filter in values(self.filters) if filter.freetext ]) def get_query_string(self): """ Based on the data in the request, return the equivalent query string that you can use with parse_query_string() to create a query set. """ form = self.form request = self.get_request() if request is None: return '' if request_data(request).get(self.get_advanced_query_param(), '').strip(): return request_data(request).get(self.get_advanced_query_param()) elif form.is_valid(): def expr(field, is_list, value): if is_list: return '(' + ' OR '.join([ expr(field, is_list=False, value=x) for x in field.value ]) + ')' return build_query_expression(field=field, filter=self.filters[field._name], value=value) result = [ expr(field, field.is_list, field.value) for field in values(form.fields) if field._name != FREETEXT_SEARCH_NAME and field.value not in ( None, '', []) ] if FREETEXT_SEARCH_NAME in form.fields: freetext = form.fields[FREETEXT_SEARCH_NAME].value if freetext: result.append('(%s)' % ' or '.join([ f'{filter._name}:{to_string_surrounded_by_quote(freetext)}' for filter in values(self.filters) if filter.freetext ])) return ' and '.join(result) else: return '' def get_q(self): """ Create a query set based on the data in the request. """ try: return self.parse_query_string(self.get_query_string()) except QueryException as e: self.query_error = str(e) raise @classmethod @dispatch( filters=EMPTY, ) def filters_from_model(cls, filters, **kwargs): return create_members_from_model( member_class=cls.get_meta().member_class, member_params_by_member_name=filters, **kwargs) @classmethod @dispatch( filters=EMPTY, ) def _from_model(cls, *, rows=None, model=None, filters, include=None, exclude=None): assert rows is None or isinstance(rows, QuerySet), \ 'auto__rows needs to be a QuerySet for filter generation to work. ' \ 'If it needs to be a lambda, provide a model with auto__model for filter generation, ' \ 'and pass the lambda as rows.' model, rows = model_and_rows(model, rows) assert model is not None or rows is not None, "auto__model or auto__rows must be specified" filters = cls.filters_from_model(model=model, include=include, exclude=exclude, filters=filters) return model, rows, filters
class Meta: page_size = 3 # And register a button that will get the selection passed in its post_handler bulk__actions__print = Action.primary( display_name='print me', post_handler=bulk__actions__print__post_handler)
def test_template(): assert Action(template=Template('{{action.group}}'), group='foo').bind(request=None).__html__() == 'foo'
def test_render_submit(): submit = Action.submit(display_name='Do it').bind() assert_renders( submit, ''' <input accesskey="s" name="-" type="submit" value="Do it"/> ''')
def test_delete_action(): assert Action.delete().bind(request=None).__html__() == '<button accesskey="s" name="-">Submit</button>'
def test_icon_action(): assert Action.icon('foo', display_name='dn').bind(request=None).__html__() == '<a><i class="fa fa-foo"></i> dn</a>'
def test_icon_action_with_icon_classes(): assert Action.icon('foo', display_name='dn', icon_classes=['a', 'b']).bind(request=None).__html__() == '<a><i class="fa fa-foo fa-a fa-b"></i> dn</a>'
def test_display_name_to_value_attr(): assert Action.delete(display_name='foo').bind(request=None).__html__() == '<button accesskey="s" name="-">foo</button>'
def test_lambda_tag(): assert Action(tag=lambda action, **_: 'foo', display_name='').bind(request=None).__html__() == '<foo></foo>'