diff options
| -rw-r--r-- | debug_toolbar/panels/__init__.py | 14 | ||||
| -rw-r--r-- | debug_toolbar/static/debug_toolbar/js/toolbar.js | 17 | ||||
| -rw-r--r-- | debug_toolbar/templates/debug_toolbar/base.html | 13 | ||||
| -rw-r--r-- | debug_toolbar/toolbar/loader.py | 25 | ||||
| -rw-r--r-- | debug_toolbar/urls.py | 1 | ||||
| -rw-r--r-- | debug_toolbar/views.py | 21 | ||||
| -rw-r--r-- | tests/__init__.py | 10 | ||||
| -rw-r--r-- | tests/panels/test_logger.py | 30 | ||||
| -rw-r--r-- | tests/panels/test_profiling.py | 29 | ||||
| -rw-r--r-- | tests/panels/test_request_vars.py | 35 | ||||
| -rw-r--r-- | tests/panels/test_sql.py | 42 | ||||
| -rw-r--r-- | tests/panels/test_template.py | 22 | ||||
| -rw-r--r-- | tests/test_integration.py | 43 | ||||
| -rw-r--r-- | tests/tests.py | 2 | ||||
| -rw-r--r-- | tests/urls.py | 3 | ||||
| -rw-r--r-- | tests/views.py | 14 |
16 files changed, 208 insertions, 113 deletions
diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index 8be87e1..eb3b21f 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from django.template.defaultfilters import slugify from django.template.loader import render_to_string -from debug_toolbar.middleware import DebugToolbarMiddleware class DebugPanel(object): @@ -11,14 +10,15 @@ class DebugPanel(object): """ # name = 'Base' # template = 'debug_toolbar/panels/base.html' - has_content = False # If content returns something, set to true in subclass + has_content = False # If content returns something, set to True in subclass # We'll maintain a local context instance so we can expose our template # context variables to panels which need them: context = {} # Panel methods - def __init__(self, context={}): + def __init__(self, toolbar, context={}): + self.toolbar = toolbar self.context.update(context) self.slug = slugify(self.name) @@ -44,16 +44,14 @@ class DebugPanel(object): return render_to_string(self.template, context) def record_stats(self, stats): - toolbar = DebugToolbarMiddleware.get_current() - panel_stats = toolbar.stats.get(self.slug) + panel_stats = self.toolbar.stats.get(self.slug) if panel_stats: panel_stats.update(stats) else: - toolbar.stats[self.slug] = stats + self.toolbar.stats[self.slug] = stats def get_stats(self): - toolbar = DebugToolbarMiddleware.get_current() - return toolbar.stats.get(self.slug, {}) + return self.toolbar.stats.get(self.slug, {}) # Standard middleware methods diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 094c3ac..0b7d1ec 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -28,6 +28,23 @@ $(this).parent().removeClass('active'); } else { $('.panelContent').hide(); // Hide any that are already open + var inner = current.find('.djDebugPanelContent .scroll').first(); + if ($(inner).empty()) { + var ajax_data = { + data: { + toolbar_id: $('#djDebug').data('toolbar-id'), + panel_id: this.className + }, + type: 'GET', + url: $('#djDebug').data('render-panel-url') + }; + $.ajax(ajax_data).done(function(data){ + inner.html(data); + }).fail(function(xhr){ + var message = '<div class="djDebugPanelTitle"><a class="djDebugClose djDebugBack" href="">Back</a><h3>'+xhr.status+': '+xhr.statusText+'</h3></div>'; + $('#djDebugWindow').html(message).show(); + }); + } current.show(); $('#djDebugToolbar li').removeClass('active'); $(this).parent().addClass('active'); diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index e214044..d5a356c 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -8,7 +8,9 @@ if(!window.jQuery) document.write('<scr'+'ipt src="{{ STATIC_URL }}debug_toolbar //]]></script> <script src="{{ STATIC_URL }}debug_toolbar/js/jquery.cookie.js"></script> <script src="{{ STATIC_URL }}debug_toolbar/js/toolbar.js"></script> -<div id="djDebug" style="display:none;" dir="ltr" {{ TOOLBAR_ROOT_TAG_ATTRS|safe }}> +<div id="djDebug" style="display:none;" dir="ltr" + data-toolbar-id="{{ toolbar_id }}" data-render-panel-url="/__debug__/render_panel/" + {{ TOOLBAR_ROOT_TAG_ATTRS|safe }}> <div style="display:none;" id="djDebugToolbar"> <ul id="djDebugPanelList"> {% if panels %} @@ -22,7 +24,7 @@ if(!window.jQuery) document.write('<scr'+'ipt src="{{ STATIC_URL }}debug_toolbar {% if panel.has_content and panel.enabled %} <a href="{{ panel.url|default:"#" }}" title="{{ panel.title }}" class="{{ panel.dom_id }}"> {% else %} - <div class="contentless{% if panel.disabled %} disabled{% endif %}"> + <div class="contentless{% if panel.disabled %} disabled{% endif %}"> {% endif %} {{ panel.nav_title }} {% if panel.enabled %} @@ -33,7 +35,7 @@ if(!window.jQuery) document.write('<scr'+'ipt src="{{ STATIC_URL }}debug_toolbar {% if panel.has_content and panel.enabled %} </a> {% else %} - </div> + </div> {% endif %} </li> {% endfor %} @@ -50,9 +52,8 @@ if(!window.jQuery) document.write('<scr'+'ipt src="{{ STATIC_URL }}debug_toolbar <h3>{{ panel.title|safe }}</h3> </div> <div class="djDebugPanelContent"> - <div class="scroll"> - {{ panel.content|safe }} - </div> + <div class="scroll"> + </div> </div> </div> {% endif %} diff --git a/debug_toolbar/toolbar/loader.py b/debug_toolbar/toolbar/loader.py index c8630a9..de4aa93 100644 --- a/debug_toolbar/toolbar/loader.py +++ b/debug_toolbar/toolbar/loader.py @@ -45,8 +45,7 @@ class DebugToolbar(object): """ global panel_classes for panel_class in panel_classes: - panel_instance = panel_class(context=self.template_context) - + panel_instance = panel_class(self, context=self.template_context) self._panels[panel_class] = panel_instance def render_toolbar(self): @@ -56,8 +55,8 @@ class DebugToolbar(object): context = self.template_context.copy() context.update({ 'panels': self.panels, + 'toolbar_id': save_toolbar(self), }) - return render_to_string('debug_toolbar/base.html', context) @@ -101,3 +100,23 @@ def load_panel_classes(): 'Toolbar Panel module "%s" does not define a "%s" class' % (panel_module, panel_classname)) panel_classes.append(panel_class) + + +toolbar_counter = 0 +toolbar_maxsize = 10 # keep data for the last 10 requests +toolbar_results = SortedDict() + + +def save_toolbar(toolbar): + global toolbar_counter, toolbar_results + toolbar_counter += 1 + toolbar_results[toolbar_counter] = toolbar + for _ in range(len(toolbar_results) - toolbar_maxsize): + # When we drop support for Python 2.6 and switch to + # collections.OrderedDict, use popitem(last=False). + del toolbar_results[toolbar_results.keyOrder[0]] + return toolbar_counter + + +def get_saved_toolbar(toolbar_id): + return toolbar_results.get(toolbar_id) diff --git a/debug_toolbar/urls.py b/debug_toolbar/urls.py index 1a6daf8..05ef245 100644 --- a/debug_toolbar/urls.py +++ b/debug_toolbar/urls.py @@ -12,6 +12,7 @@ from django.conf.urls import patterns, url _PREFIX = '__debug__' urlpatterns = patterns('debug_toolbar.views', # noqa + url(r'^%s/render_panel/$' % _PREFIX, 'render_panel', name='render_panel'), url(r'^%s/sql_select/$' % _PREFIX, 'sql_select', name='sql_select'), url(r'^%s/sql_explain/$' % _PREFIX, 'sql_explain', name='sql_explain'), url(r'^%s/sql_profile/$' % _PREFIX, 'sql_profile', name='sql_profile'), diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py index 8b3e63c..21f4747 100644 --- a/debug_toolbar/views.py +++ b/debug_toolbar/views.py @@ -6,11 +6,30 @@ views in any other way is generally not advised. from __future__ import unicode_literals -from django.http import HttpResponseBadRequest +from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import render +from django.utils.html import escape +from django.utils.translation import ugettext as _ from django.views.decorators.csrf import csrf_exempt from debug_toolbar.forms import SQLSelectForm +from debug_toolbar.toolbar.loader import get_saved_toolbar + + +def render_panel(request): + """Render the contents of a panel""" + toolbar = get_saved_toolbar(int(request.GET['toolbar_id'])) + if toolbar is None: + content = _("Data for this panel isn't available anymore. " + "Please reload the page and retry.") + content = "<p>%s</p>" % escape(content) + else: + panel_id = request.GET['panel_id'] + for panel in toolbar.panels: + if panel.dom_id() == panel_id: + content = panel.content() + break + return HttpResponse(content) @csrf_exempt diff --git a/tests/__init__.py b/tests/__init__.py index 3b83e4c..47fba24 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,8 @@ # Refresh the debug toolbar's configuration when overriding settings. from debug_toolbar.utils.settings import CONFIG, CONFIG_DEFAULTS +from debug_toolbar.toolbar.loader import load_panel_classes, panel_classes # noqa + from django.dispatch import receiver from django.test.signals import setting_changed @@ -10,3 +12,11 @@ def update_toolbar_config(**kwargs): if kwargs['setting'] == 'DEBUG_TOOLBAR_CONFIG': CONFIG.update(CONFIG_DEFAULTS) CONFIG.update(kwargs['value'] or {}) + + +@receiver(setting_changed) +def update_toolbar_panels(**kwargs): + if kwargs['setting'] == 'DEBUG_TOOLBAR_PANELS': + global panel_classes + panel_classes = [] # noqa + load_panel_classes() diff --git a/tests/panels/test_logger.py b/tests/panels/test_logger.py index 477d100..9a3ac37 100644 --- a/tests/panels/test_logger.py +++ b/tests/panels/test_logger.py @@ -10,25 +10,26 @@ from ..base import BaseTestCase class LoggingPanelTestCase(BaseTestCase): + def setUp(self): + super(LoggingPanelTestCase, self).setUp() + self.panel = self.toolbar.get_panel(LoggingPanel) + self.logger = logging.getLogger(__name__) + def test_happy_case(self): - logger = logging.getLogger(__name__) - logger.info('Nothing to see here, move along!') + self.logger.info('Nothing to see here, move along!') - logging_panel = self.toolbar.get_panel(LoggingPanel) - logging_panel.process_response(None, None) - records = logging_panel.get_stats()['records'] + self.panel.process_response(self.request, self.response) + records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) self.assertEqual('Nothing to see here, move along!', records[0]['message']) def test_formatting(self): - logger = logging.getLogger(__name__) - logger.info('There are %d %s', 5, 'apples') + self.logger.info('There are %d %s', 5, 'apples') - logging_panel = self.toolbar.get_panel(LoggingPanel) - logging_panel.process_response(None, None) - records = logging_panel.get_stats()['records'] + self.panel.process_response(self.request, self.response) + records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) self.assertEqual('There are 5 apples', @@ -39,14 +40,11 @@ class LoggingPanelTestCase(BaseTestCase): def __str__(self): raise Exception('Please not stringify me!') - logger = logging.getLogger(__name__) - # should not raise exception, but fail silently - logger.debug('This class is misbehaving: %s', BadClass()) + self.logger.debug('This class is misbehaving: %s', BadClass()) - logging_panel = self.toolbar.get_panel(LoggingPanel) - logging_panel.process_response(None, None) - records = logging_panel.get_stats()['records'] + self.panel.process_response(self.request, self.response) + records = self.panel.get_stats()['records'] self.assertEqual(1, len(records)) self.assertEqual(MESSAGE_IF_STRING_REPRESENTATION_INVALID, diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py new file mode 100644 index 0000000..6a7823e --- /dev/null +++ b/tests/panels/test_profiling.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals + +from django.contrib.auth.models import User +from django.db import IntegrityError, transaction +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(DEBUG=True, + DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.profiling.ProfilingDebugPanel']) +class ProfilingPanelIntegrationTestCase(TestCase): + + urls = 'tests.urls' + + def test_view_executed_once(self): + + self.assertEqual(User.objects.count(), 0) + + response = self.client.get('/new_user/') + self.assertContains(response, 'Profiling') + self.assertEqual(User.objects.count(), 1) + + with self.assertRaises(IntegrityError): + if hasattr(transaction, 'atomic'): # Django >= 1.6 + with transaction.atomic(): + response = self.client.get('/new_user/') + else: + response = self.client.get('/new_user/') + self.assertEqual(User.objects.count(), 1) diff --git a/tests/panels/test_request_vars.py b/tests/panels/test_request_vars.py new file mode 100644 index 0000000..5366105 --- /dev/null +++ b/tests/panels/test_request_vars.py @@ -0,0 +1,35 @@ +# coding: utf-8 + +from __future__ import unicode_literals + +from django.utils import six + +from debug_toolbar.panels.request_vars import RequestVarsDebugPanel + +from ..base import BaseTestCase + + +class RequestVarsDebugPanelTestCase(BaseTestCase): + + def setUp(self): + super(RequestVarsDebugPanelTestCase, self).setUp() + self.panel = self.toolbar.get_panel(RequestVarsDebugPanel) + + def test_non_ascii_session(self): + self.request.session = {'où': 'où'} + if not six.PY3: + self.request.session['là'.encode('utf-8')] = 'là'.encode('utf-8') + self.panel.process_request(self.request) + self.panel.process_response(self.request, self.response) + content = self.panel.content() + if six.PY3: + self.assertIn('où', content) + else: + self.assertIn('o\\xf9', content) + self.assertIn('l\\xc3\\xa0', content) + + def test_object_with_non_ascii_repr_in_request_vars(self): + self.request.path = '/non_ascii_request/' + self.panel.process_request(self.request) + self.panel.process_response(self.request, self.response) + self.assertIn('nôt åscíì', self.panel.content()) diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py index 10846f5..a1de0ea 100644 --- a/tests/panels/test_sql.py +++ b/tests/panels/test_sql.py @@ -14,15 +14,18 @@ from ..base import BaseTestCase class SQLPanelTestCase(BaseTestCase): + def setUp(self): + super(SQLPanelTestCase, self).setUp() + self.panel = self.toolbar.get_panel(SQLDebugPanel) + def test_recording(self): - panel = self.toolbar.get_panel(SQLDebugPanel) - self.assertEqual(len(panel._queries), 0) + self.assertEqual(len(self.panel._queries), 0) list(User.objects.all()) # ensure query was logged - self.assertEqual(len(panel._queries), 1) - query = panel._queries[0] + self.assertEqual(len(self.panel._queries), 1) + query = self.panel._queries[0] self.assertEqual(query[0], 'default') self.assertTrue('sql' in query[1]) self.assertTrue('duration' in query[1]) @@ -32,16 +35,24 @@ class SQLPanelTestCase(BaseTestCase): self.assertTrue(len(query[1]['stacktrace']) > 0) def test_non_ascii_query(self): - panel = self.toolbar.get_panel(SQLDebugPanel) - self.assertEqual(len(panel._queries), 0) + self.assertEqual(len(self.panel._queries), 0) + + # non-ASCII text query + list(User.objects.extra(where=["username = 'apéro'"])) + self.assertEqual(len(self.panel._queries), 1) + + # non-ASCII text parameters + list(User.objects.filter(username='thé')) + self.assertEqual(len(self.panel._queries), 2) + + # non-ASCII bytes parameters + list(User.objects.filter(username='café'.encode('utf-8'))) + self.assertEqual(len(self.panel._queries), 3) - # non-ASCII query - list(User.objects.extra(where=["username = 'café'"])) - self.assertEqual(len(panel._queries), 1) + self.panel.process_response(self.request, self.response) - # non-ASCII parameters - list(User.objects.filter(username='café')) - self.assertEqual(len(panel._queries), 2) + # ensure the panel renders correctly + self.assertIn('café', self.panel.content()) @unittest.skipUnless(connection.vendor == 'postgresql', 'Test valid only on PostgreSQL') @@ -55,15 +66,14 @@ class SQLPanelTestCase(BaseTestCase): self.assertTrue('erroneous query' in str(e)) def test_disable_stacktraces(self): - panel = self.toolbar.get_panel(SQLDebugPanel) - self.assertEqual(len(panel._queries), 0) + self.assertEqual(len(self.panel._queries), 0) with self.settings(DEBUG_TOOLBAR_CONFIG={'ENABLE_STACKTRACES': False}): list(User.objects.all()) # ensure query was logged - self.assertEqual(len(panel._queries), 1) - query = panel._queries[0] + self.assertEqual(len(self.panel._queries), 1) + query = self.panel._queries[0] self.assertEqual(query[0], 'default') self.assertTrue('sql' in query[1]) self.assertTrue('duration' in query[1]) diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 4e30a73..c98521e 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -1,3 +1,5 @@ +# coding: utf-8 + from __future__ import unicode_literals import django @@ -8,13 +10,16 @@ from debug_toolbar.panels.template import TemplateDebugPanel from debug_toolbar.panels.sql import SQLDebugPanel from ..base import BaseTestCase +from ..models import NonAsciiRepr class TemplateDebugPanelTestCase(BaseTestCase): + def setUp(self): + super(TemplateDebugPanelTestCase, self).setUp() + self.panel = self.toolbar.get_panel(TemplateDebugPanel) + def test_queryset_hook(self): - template_panel = self.toolbar.get_panel(TemplateDebugPanel) - sql_panel = self.toolbar.get_panel(SQLDebugPanel) t = Template("No context variables here!") c = Context({ 'queryset': User.objects.all(), @@ -23,9 +28,20 @@ class TemplateDebugPanelTestCase(BaseTestCase): } }) t.render(c) + # ensure the query was NOT logged + sql_panel = self.toolbar.get_panel(SQLDebugPanel) self.assertEqual(len(sql_panel._queries), 0) + base_ctx_idx = 1 if django.VERSION[:2] >= (1, 5) else 0 - ctx = template_panel.templates[0]['context'][base_ctx_idx] + ctx = self.panel.templates[0]['context'][base_ctx_idx] self.assertIn('<<queryset of auth.User>>', ctx) self.assertIn('<<triggers database query>>', ctx) + + def test_object_with_non_ascii_repr_in_context(self): + self.panel.process_request(self.request) + t = Template("{{ object }}") + c = Context({'object': NonAsciiRepr()}) + t.render(c) + self.panel.process_response(self.request, self.response) + self.assertIn('nôt åscíì', self.panel.content()) diff --git a/tests/test_integration.py b/tests/test_integration.py index 41206fc..4bb0d38 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals from xml.etree import ElementTree as ET -from django.contrib.auth.models import User -from django.db import IntegrityError, transaction from django.test import TestCase, RequestFactory from django.test.utils import override_settings from django.utils import six @@ -139,47 +137,6 @@ class DebugToolbarIntegrationTestCase(TestCase): self.assertContains(response, 'LÀTÍN') # template self.assertContains(response, 'djDebug') # toolbar - def test_non_ascii_bytes_in_db_params(self): - response = self.client.get('/non_ascii_bytes_in_db_params/') - if six.PY3: - self.assertContains(response, 'djàngó') - else: - self.assertContains(response, 'dj\\xe0ng\\xf3') - - def test_non_ascii_session(self): - response = self.client.get('/set_session/') - if six.PY3: - self.assertContains(response, 'où') - else: - self.assertContains(response, 'o\\xf9') - self.assertContains(response, 'l\\xc3\\xa0') - - def test_object_with_non_ascii_repr_in_context(self): - response = self.client.get('/non_ascii_context/') - self.assertContains(response, 'nôt åscíì') - - def test_object_with_non_ascii_repr_in_request_vars(self): - response = self.client.get('/non_ascii_request/') - self.assertContains(response, 'nôt åscíì') - def test_xml_validation(self): response = self.client.get('/regular/XML/') ET.fromstring(response.content) # shouldn't raise ParseError - - def test_view_executed_once(self): - with self.settings( - DEBUG_TOOLBAR_PANELS=['debug_toolbar.panels.profiling.ProfilingDebugPanel']): - - self.assertEqual(User.objects.count(), 0) - - response = self.client.get('/new_user/') - self.assertContains(response, 'Profiling') - self.assertEqual(User.objects.count(), 1) - - with self.assertRaises(IntegrityError): - if hasattr(transaction, 'atomic'): # Django >= 1.6 - with transaction.atomic(): - response = self.client.get('/new_user/') - else: - response = self.client.get('/new_user/') - self.assertEqual(User.objects.count(), 1) diff --git a/tests/tests.py b/tests/tests.py index 10e0a9b..e373d51 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -3,6 +3,8 @@ import django if django.VERSION[:2] < (1, 6): # unittest-style discovery isn't available from .commands.test_debugsqlshell import * # noqa from .panels.test_logger import * # noqa + from .panels.test_profiling import * # noqa + from .panels.test_request_vars import * # noqa from .panels.test_sql import * # noqa from .panels.test_template import * # noqa from .test_integration import * # noqa diff --git a/tests/urls.py b/tests/urls.py index 93e77c4..977084b 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -11,13 +11,10 @@ from .models import NonAsciiRepr admin.autodiscover() urlpatterns = patterns('tests.views', # noqa - url(r'^set_session/$', 'set_session'), url(r'^resolving1/(.+)/(.+)/$', 'resolving_view', name='positional-resolving'), url(r'^resolving2/(?P<arg1>.+)/(?P<arg2>.+)/$', 'resolving_view'), url(r'^resolving3/(.+)/$', 'resolving_view', {'arg2': 'default'}), url(r'^regular/(?P<title>.*)/$', 'regular_view'), - url(r'^non_ascii_context/$', 'non_ascii_context'), - url(r'^non_ascii_bytes_in_db_params/$', 'new_user', {'username': 'djàngó'.encode('utf-8')}), url(r'^non_ascii_request/$', 'regular_view', {'title': NonAsciiRepr()}), url(r'^new_user/$', 'new_user'), url(r'^execute_sql/$', 'execute_sql'), diff --git a/tests/views.py b/tests/views.py index 01c4971..d7ff167 100644 --- a/tests/views.py +++ b/tests/views.py @@ -5,9 +5,6 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse from django.shortcuts import render -from django.utils import six - -from .models import NonAsciiRepr def execute_sql(request): @@ -15,10 +12,6 @@ def execute_sql(request): return HttpResponse() -def non_ascii_context(request): - return render(request, 'basic.html', {'title': NonAsciiRepr()}) - - def regular_view(request, title): return render(request, 'basic.html', {'title': title}) @@ -31,10 +24,3 @@ def new_user(request, username='joe'): def resolving_view(request, arg1, arg2): # see test_url_resolving in tests.py return HttpResponse() - - -def set_session(request): - request.session['où'] = 'où' - if not six.PY3: - request.session['là'.encode('utf-8')] = 'là'.encode('utf-8') - return render(request, 'basic.html') |
