def test_as_html(): # str case assert format_html('{}', as_html(part='foo', context={})) == 'foo' assert format_html('{}', as_html(part='<foo>bar</foo>', context={})) == '<foo>bar</foo>' assert format_html('{}', as_html(part=mark_safe('<foo>bar</foo>'), context={})) == '<foo>bar</foo>' # Template case c = RequestContext(req('get')) assert format_html('{}', as_html(part=Template('foo'), context=c)) == 'foo' assert format_html('{}', as_html(part=Template('<foo>bar</foo>'), context=c)) == '<foo>bar</foo>' # __html__ attribute case assert format_html( '{}', as_html(part=Struct(__html__=lambda context: 'foo'), context={})) == 'foo' assert format_html( '{}', as_html(part=Struct(__html__=lambda context: '<foo>bar</foo>'), context={})) == '<foo>bar</foo>' assert format_html( '{}', as_html( part=Struct(__html__=lambda context: mark_safe('<foo>bar</foo>')), context={})) == '<foo>bar</foo>'
def as_html(*, request=None, part: PartType, context): if isinstance(part, str): return part elif isinstance(part, template_types): from django.template import RequestContext assert not isinstance(context, RequestContext) template = part return mark_safe(template.render(context=RequestContext(request, context))) elif hasattr(part, '__html__'): return part.__html__() else: return str(part)
def render_attrs(attrs): """ Render HTML attributes, or return '' if no attributes needs to be rendered. """ if not attrs: return '' def parts(): for key, value in sorted(attrs.items()): if value is None: continue if value is True: yield f'{key}' continue if isinstance(value, dict): if key == 'class': if not value: continue value = render_class(value) if not value: continue elif key == 'style': if not value: continue value = render_style(value) if not value: continue else: raise TypeError( f'Only the class and style attributes can be dicts, you sent {value} for key {key}' ) elif isinstance(value, (list, tuple)): raise TypeError( f"Attributes can't be of type {type(value).__name__}, you sent {value} for key {key}" ) elif callable(value): from .docs import get_docs_callable_description raise TypeError( f"Attributes can't be callable, you sent {get_docs_callable_description(value)} for key {key}" ) v = f'{value}'.replace('"', '"') yield f'{key}="{v}"' r = mark_safe(' %s' % ' '.join(parts())) return '' if r == ' ' else r
def live_edit_view(request, view, args, kwargs): view = get_wrapped_view(view) # Read the old code try: # view is a function based view filename = view.__globals__['__file__'] except AttributeError: # view is an iommi class from iommi.debug import filename_and_line_num_from_part filename, _ = filename_and_line_num_from_part(view) with open(filename) as f: entire_file = f.read() ast_of_entire_file = parso.parse(entire_file) is_unix_line_endings = '\r\n' not in entire_file ast_of_old_code = find_view(view, ast_of_entire_file) assert ast_of_old_code is not None flow_direction = request.GET.get('_iommi_live_edit') or 'column' assert flow_direction in ('column', 'row') if request.method == 'POST': try: code = request.POST['data'].replace('\t', ' ') if is_unix_line_endings: code = code.replace('\r\n', '\n') final_result = dangerous_execute_code(code, request, view, args, kwargs) if orig_reload is not None: # A little monkey patch dance to avoid one reload of the runserver when it's just us writing the code to disk # This only works in django 2.2+ def restore_auto_reload(filename): from django.utils import autoreload print('Skipped reload') autoreload.trigger_reload = orig_reload autoreload.trigger_reload = restore_auto_reload if isinstance(view, Part): ast_of_new_code = find_node(name=view.__class__.__name__, node=parso.parse(code), node_type='classdef') else: ast_of_new_code = find_node(name=view.__name__, node=parso.parse(code), node_type='funcdef') ast_of_old_code.children[:] = ast_of_new_code.children new_code = ast_of_entire_file.get_code() with open(filename, 'w') as f: f.write(new_code) return final_result except Exception as e: import traceback traceback.print_exc() error = str(e) if not error: error = str(e.__class__) return HttpResponse(json.dumps(dict(error=error))) return LiveEditPage( title='iommi live edit', h_tag__include=False, assets__code_editor=Asset.js( attrs=dict( src='https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js', integrity= 'sha512-GZ1RIgZaSc8rnco/8CXfRdCpDxRCphenIiZ2ztLy3XQfCbQUSCuk8IudvNHxkRA3oUg6q0qejgN/qqyG1duv5Q==', crossorigin='anonymous', ), after=-1, ), assets__live_edit_page_custom=Asset( tag='style', text=''' .container { padding: 0 !important; margin: 0 !important; max-width: 100%; } html, body { height: 100%; margin: 0; } .container { display: flex; flex-flow: <<flow_direction>>; height: 100%; } .container iframe { flex: 1 1 auto; } .container #editor { flex: 2 1 auto; } '''.replace('<<flow_direction>>', flow_direction), ), parts__result=html.iframe(attrs__id='result'), parts__editor=html.div( ast_of_old_code.get_code(), attrs__id='editor', ), parts__script=html.script( mark_safe(''' function iommi_debounce(func, wait) { let timeout; return (...args) => { const fn = () => func.apply(this, args); clearTimeout(timeout); timeout = setTimeout(() => fn(), wait); }; } var editor = ace.edit("editor"); editor.setTheme("ace/theme/cobalt"); editor.session.setMode("ace/mode/python"); editor.setShowPrintMargin(false); async function update() { let form_data = new FormData(); form_data.append('data', editor.getValue()); let response = await fetch('', { method: 'POST', body: form_data }); let foo = await response.json(); if (foo.page) { // TODO: get scroll position and restore it document.getElementById('result').srcdoc = foo.page; } } function foo() { iommi_debounce(update, 200)(); } editor.session.on('change', foo); editor.setFontSize(14); editor.session.setUseWrapMode(true); foo(); ''')), )
def iommi_debug_panel(part): filename, lineno = filename_and_line_num_from_part(part) if filename is None or filename.endswith('urls.py'): import inspect if not inspect.getmodule(type(part)).__name__.startswith('iommi.'): filename = inspect.getsourcefile(type(part)) lineno = inspect.getsourcelines(type(part))[-1] if filename is not None: source_url = src_debug_url_builder(filename, lineno) else: source_url = None script = r""" window.iommi_start_pick = function() { window.iommi_pick_stack = []; function create(html) { let r = document.createElement('div'); r.innerHTML = html; return r.firstChild; } window.iommi_close_pick_toolbar = function() { window.iommi_pick_stack.forEach(function(el) { el[3].style.backgroundColor = el[2]; }); document.getElementById('iommi-pick-toolbar').remove() }; function update_toolbar() { let toolbar = document.getElementById('iommi-pick-toolbar'); if (!toolbar) { return; } while(toolbar.firstChild) { toolbar.removeChild(toolbar.firstChild); } toolbar.append(create('<div style="float: right" onclick="iommi_close_pick_toolbar()">close</div>')); for (let i in window.iommi_pick_stack) { let x = window.iommi_pick_stack[i]; toolbar.append(create('<div style="background-color: ' + getColor(i) + '">' + x[0] + ' <a href="https://docs.iommi.rocks/en/latest/' + x[1] + '.html">' + x[1] + '</a></div>')); } } let with_iommi_path = document.querySelectorAll('*[data-iommi-path]'); let colors = [ 'rgb(255, 255, 191)', 'rgb(254, 224, 139)', 'rgb(253, 174, 97)', 'rgb(244, 109, 67)', 'rgb(213, 62, 79)', 'rgb(158, 1, 66)', 'rgb(230, 245, 152)', 'rgb(171, 221, 164)', 'rgb(102, 194, 165)', 'rgb( 50, 136, 189)', 'rgb( 94, 79, 162)', ]; function getColor(index) { return colors[Math.min(index, colors.length - 1)] } function mouseenter() { window.iommi_pick_stack.push([this.getAttribute('data-iommi-path'), this.getAttribute('data-iommi-type'), this.style.backgroundColor, this]) this.style.backgroundColor = getColor(window.iommi_pick_stack.length-1); update_toolbar(); } function mouseleave() { if (window.iommi_pick_stack.length) { this.style.backgroundColor = window.iommi_pick_stack.pop()[2]; update_toolbar(); } } function click() { document.querySelectorAll('*[data-iommi-path]').forEach(function (e) { e.removeEventListener('mouseenter', mouseenter) e.removeEventListener('mouseleave', mouseleave) e.removeEventListener('click', click) }); } with_iommi_path.forEach(function (e) { e.addEventListener('mouseenter', mouseenter); e.addEventListener('mouseleave', mouseleave); setTimeout(function(){ e.addEventListener('click', click); }); }); let toolbar = create('<div id="iommi-pick-toolbar" style="position: fixed; left: 0; bottom: 0; width: 100%; background-color: white; color: black; padding: 4px; border-top: 2px solid #1084ff; z-index: 200">'); document.getElementsByTagName('body')[0].append(toolbar); }; """ from iommi.menu import DebugMenu return DebugMenu(sub_menu__code__url=source_url).bind(request=part.get_request()).__html__() + mark_safe(f'<script>{script}</script>')
def live_edit_view(request, view_func): # Read the old code filename = view_func.__globals__['__file__'] with open(filename) as f: entire_file = f.read() ast_of_entire_file = parso.parse(entire_file) is_unix_line_endings = '\r\n' not in entire_file ast_of_old_code = find_function(name=view_func.__name__, node=ast_of_entire_file) flow_direction = request.GET.get('_iommi_live_edit') or 'column' assert flow_direction in ('column', 'row') if request.method == 'POST': try: code = request.POST['data'].replace('\t', ' ') if is_unix_line_endings: code = code.replace('\r\n', '\n') local_variables = {} exec(code, view_func.__globals__, local_variables) assert len(local_variables) == 1 request.method = 'GET' response = list(local_variables.values())[0](request) response = render_if_needed(request, response) final_result = HttpResponse( json.dumps(dict(page=response.content.decode()))) ast_of_new_code = find_function(name=view_func.__name__, node=parso.parse(code)) ast_of_old_code.children[:] = ast_of_new_code.children # This only works in django 2.2+ if orig_reload is not None: # A little monkey patch dance to avoid one reload of the runserver when it's just us writing the code to disk def restore_auto_reload(filename): from django.utils import autoreload print('Skipped reload') autoreload.trigger_reload = orig_reload autoreload.trigger_reload = restore_auto_reload new_code = ast_of_entire_file.get_code() with open(filename, 'w') as f: f.write(new_code) return final_result except Exception as e: return HttpResponse(json.dumps(dict(error=str(e)))) # This class exists just to provide a way to style the page class LiveEditPage(Page): pass return LiveEditPage( assets__code_editor=Asset.js( attrs=dict( src='https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js', integrity= 'sha512-GZ1RIgZaSc8rnco/8CXfRdCpDxRCphenIiZ2ztLy3XQfCbQUSCuk8IudvNHxkRA3oUg6q0qejgN/qqyG1duv5Q==', crossorigin='anonymous', ), after=-1, ), assets__custom=Asset(tag='style', text=''' .container { padding: 0 !important; margin: 0 !important; max-width: 100%; } html, body { height: 100%; margin: 0; } .container { display: flex; flex-flow: <<flow_direction>>; height: 100%; } .container iframe { flex: 1 1 auto; } .container #editor { flex: 2 1 auto; } '''.replace('<<flow_direction>>', flow_direction)), parts__result=html.iframe(attrs__id='result'), parts__editor=html.div( ast_of_old_code.get_code(), attrs__id='editor', ), parts__script=html.script( mark_safe(''' function iommi_debounce(func, wait) { let timeout; return (...args) => { const fn = () => func.apply(this, args); clearTimeout(timeout); timeout = setTimeout(() => fn(), wait); }; } var editor = ace.edit("editor"); editor.setTheme("ace/theme/cobalt"); editor.session.setMode("ace/mode/python"); editor.setShowPrintMargin(false); async function update() { let form_data = new FormData(); form_data.append('data', editor.getValue()); let response = await fetch('', { method: 'POST', body: form_data }); let foo = await response.json(); if (foo.page) { // TODO: get scroll position and restore it document.getElementById('result').srcdoc = foo.page; } } function foo() { iommi_debounce(update, 200)(); } editor.session.on('change', foo); editor.setFontSize(14); editor.session.setUseWrapMode(true); foo(); ''')), )