diff options
| author | Jason Pellerin <jpellerin@gmail.com> | 2006-09-08 16:35:39 +0000 |
|---|---|---|
| committer | Jason Pellerin <jpellerin@gmail.com> | 2006-09-08 16:35:39 +0000 |
| commit | 84f7a2133c4d553a234165bb8cbfaf70681bb028 (patch) | |
| tree | 53d08c1ffc275a4e1d62d7f48ea81172a5919051 /tests | |
| parent | ae3896cb74d4bc42acaf6fade0d2a57e28045b2a (diff) | |
[multi-db] Merge trunk to [3737]. Some tests still failing.
git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@3739 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/modeltests/invalid_models/models.py | 37 | ||||
| -rw-r--r-- | tests/modeltests/test_client/__init__.py | 0 | ||||
| -rw-r--r-- | tests/modeltests/test_client/management.py | 10 | ||||
| -rw-r--r-- | tests/modeltests/test_client/models.py | 101 | ||||
| -rw-r--r-- | tests/modeltests/test_client/urls.py | 9 | ||||
| -rw-r--r-- | tests/modeltests/test_client/views.py | 35 | ||||
| -rw-r--r-- | tests/regressiontests/templates/tests.py | 54 | ||||
| -rw-r--r-- | tests/regressiontests/thread_isolation/tests.py | 10 | ||||
| -rwxr-xr-x | tests/runtests.py | 13 | ||||
| -rw-r--r-- | tests/templates/404.html | 1 | ||||
| -rw-r--r-- | tests/templates/500.html | 1 | ||||
| -rw-r--r-- | tests/templates/login.html | 19 | ||||
| -rw-r--r-- | tests/urls.py | 10 |
13 files changed, 272 insertions, 28 deletions
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py index 5540c1bd5f..2299cd85e6 100644 --- a/tests/modeltests/invalid_models/models.py +++ b/tests/modeltests/invalid_models/models.py @@ -68,15 +68,34 @@ class SelfClashForeign(models.Model): foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') +class ValidM2M(models.Model): + src_safe = models.CharField(maxlength=10) + validm2m = models.CharField(maxlength=10) + + # M2M fields are symmetrical by default. Symmetrical M2M fields + # on self don't require a related accessor, so many potential + # clashes are avoided. + validm2m_set = models.ManyToManyField("ValidM2M") + + m2m_1 = models.ManyToManyField("ValidM2M", related_name='id') + m2m_2 = models.ManyToManyField("ValidM2M", related_name='src_safe') + + m2m_3 = models.ManyToManyField('self') + m2m_4 = models.ManyToManyField('self') + class SelfClashM2M(models.Model): src_safe = models.CharField(maxlength=10) selfclashm2m = models.CharField(maxlength=10) - selfclashm2m_set = models.ManyToManyField("SelfClashM2M") - m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id') - m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe') - + # Non-symmetrical M2M fields _do_ have related accessors, so + # there is potential for clashes. + selfclashm2m_set = models.ManyToManyField("SelfClashM2M", symmetrical=False) + + m2m_1 = models.ManyToManyField("SelfClashM2M", related_name='id', symmetrical=False) + m2m_2 = models.ManyToManyField("SelfClashM2M", related_name='src_safe', symmetrical=False) + m2m_3 = models.ManyToManyField('self', symmetrical=False) + m2m_4 = models.ManyToManyField('self', symmetrical=False) model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "maxlength" attribute. invalid_models.fielderrors: "floatfield": FloatFields require a "decimal_places" attribute. @@ -147,9 +166,17 @@ invalid_models.selfclashforeign: Accessor for field 'foreign_2' clashes with fie invalid_models.selfclashforeign: Reverse query name for field 'foreign_2' clashes with field 'SelfClashForeign.src_safe'. Add a related_name argument to the definition for 'foreign_2'. invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'. invalid_models.selfclashm2m: Reverse query name for m2m field 'selfclashm2m_set' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'selfclashm2m_set'. +invalid_models.selfclashm2m: Accessor for m2m field 'selfclashm2m_set' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'selfclashm2m_set'. invalid_models.selfclashm2m: Accessor for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. invalid_models.selfclashm2m: Accessor for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_1' clashes with field 'SelfClashM2M.id'. Add a related_name argument to the definition for 'm2m_1'. invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_2' clashes with field 'SelfClashM2M.src_safe'. Add a related_name argument to the definition for 'm2m_2'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_3' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Accessor for m2m field 'm2m_4' clashes with related m2m field 'SelfClashM2M.selfclashm2m_set'. Add a related_name argument to the definition for 'm2m_4'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_3'. +invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. """ - diff --git a/tests/modeltests/test_client/__init__.py b/tests/modeltests/test_client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/modeltests/test_client/__init__.py diff --git a/tests/modeltests/test_client/management.py b/tests/modeltests/test_client/management.py new file mode 100644 index 0000000000..9b5a5c498e --- /dev/null +++ b/tests/modeltests/test_client/management.py @@ -0,0 +1,10 @@ +from django.dispatch import dispatcher +from django.db.models import signals +import models as test_client_app +from django.contrib.auth.models import User + +def setup_test(app, created_models, verbosity): + # Create a user account for the login-based tests + User.objects.create_user('testclient','testclient@example.com', 'password') + +dispatcher.connect(setup_test, sender=test_client_app, signal=signals.post_syncdb) diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py new file mode 100644 index 0000000000..c5b1a241ca --- /dev/null +++ b/tests/modeltests/test_client/models.py @@ -0,0 +1,101 @@ +""" +39. Testing using the Test Client + +The test client is a class that can act like a simple +browser for testing purposes. + +It allows the user to compose GET and POST requests, and +obtain the response that the server gave to those requests. +The server Response objects are annotated with the details +of the contexts and templates that were rendered during the +process of serving the request. + +Client objects are stateful - they will retain cookie (and +thus session) details for the lifetime of the Client instance. + +This is not intended as a replacement for Twill,Selenium, or +other browser automation frameworks - it is here to allow +testing against the contexts and templates produced by a view, +rather than the HTML rendered to the end-user. + +""" +from django.test.client import Client +import unittest + +class ClientTest(unittest.TestCase): + def setUp(self): + "Set up test environment" + self.client = Client() + + def test_get_view(self): + "GET a view" + response = self.client.get('/test_client/get_view/') + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['var'], 42) + self.assertEqual(response.template.name, 'GET Template') + self.failUnless('This is a test.' in response.content) + + def test_get_post_view(self): + "GET a view that normally expects POSTs" + response = self.client.get('/test_client/post_view/', {}) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template.name, 'Empty POST Template') + + def test_empty_post(self): + "POST an empty dictionary to a view" + response = self.client.post('/test_client/post_view/', {}) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template.name, 'Empty POST Template') + + def test_post_view(self): + "POST some data to a view" + post_data = { + 'value': 37 + } + response = self.client.post('/test_client/post_view/', post_data) + + # Check some response details + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['data'], '37') + self.assertEqual(response.template.name, 'POST Template') + self.failUnless('Data received' in response.content) + + def test_redirect(self): + "GET a URL that redirects elsewhere" + response = self.client.get('/test_client/redirect_view/') + + # Check that the response was a 302 (redirect) + self.assertEqual(response.status_code, 302) + + def test_unknown_page(self): + "GET an invalid URL" + response = self.client.get('/test_client/unknown_view/') + + # Check that the response was a 404 + self.assertEqual(response.status_code, 404) + + def test_view_with_login(self): + "Request a page that is protected with @login_required" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/login_protected_view/') + self.assertEqual(response.status_code, 302) + + # Request a page that requires a login + response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password') + self.assertTrue(response) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + self.assertEqual(response.template.name, 'Login Template') + + def test_view_with_bad_login(self): + "Request a page that is protected with @login, but use bad credentials" + + response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword') + self.assertFalse(response) diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py new file mode 100644 index 0000000000..09bba5c007 --- /dev/null +++ b/tests/modeltests/test_client/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * +import views + +urlpatterns = patterns('', + (r'^get_view/$', views.get_view), + (r'^post_view/$', views.post_view), + (r'^redirect_view/$', views.redirect_view), + (r'^login_protected_view/$', views.login_protected_view), +) diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py new file mode 100644 index 0000000000..bf131032eb --- /dev/null +++ b/tests/modeltests/test_client/views.py @@ -0,0 +1,35 @@ +from django.template import Context, Template +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required + +def get_view(request): + "A simple view that expects a GET request, and returns a rendered template" + t = Template('This is a test. {{ var }} is the value.', name='GET Template') + c = Context({'var': 42}) + + return HttpResponse(t.render(c)) + +def post_view(request): + """A view that expects a POST, and returns a different template depending + on whether any POST data is available + """ + if request.POST: + t = Template('Data received: {{ data }} is the value.', name='POST Template') + c = Context({'data': request.POST['value']}) + else: + t = Template('Viewing POST page.', name='Empty POST Template') + c = Context() + + return HttpResponse(t.render(c)) + +def redirect_view(request): + "A view that redirects all requests to the GET view" + return HttpResponseRedirect('/test_client/get_view/') + +@login_required +def login_protected_view(request): + "A simple view that is login protected." + t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') + c = Context({'user': request.user}) + + return HttpResponse(t.render(c)) diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 2d1ce192ef..5a8dd2d6a2 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -84,7 +84,7 @@ class Templates(unittest.TestCase): 'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"), # Fail silently when a variable is not found in the current context - 'basic-syntax04': ("as{{ missing }}df", {}, "asINVALIDdf"), + 'basic-syntax04': ("as{{ missing }}df", {}, ("asdf","asINVALIDdf")), # A variable may not contain more than one word 'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError), @@ -100,7 +100,7 @@ class Templates(unittest.TestCase): 'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"), # Fail silently when a variable's attribute isn't found - 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, "INVALID"), + 'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, ("","INVALID")), # Raise TemplateSyntaxError when trying to access a variable beginning with an underscore 'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError), @@ -116,10 +116,10 @@ class Templates(unittest.TestCase): 'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"), # Fail silently when a variable's dictionary key isn't found - 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, "INVALID"), + 'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, ("","INVALID")), # Fail silently when accessing a non-simple method - 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, "INVALID"), + 'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, ("","INVALID")), # Basic filter usage 'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"), @@ -158,7 +158,7 @@ class Templates(unittest.TestCase): 'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'), # Fail silently for methods that raise an exception with a "silent_variable_failure" attribute - 'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, "1INVALID2"), + 'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, ("12", "1INVALID2")), # In methods that raise an exception without a "silent_variable_attribute" set to True, # the exception propogates @@ -464,6 +464,14 @@ class Templates(unittest.TestCase): # translation of a constant string 'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'), + ### HANDLING OF TEMPLATE_TAG_IF_INVALID ################################### + + 'invalidstr01': ('{{ var|default:"Foo" }}', {}, ('Foo','INVALID')), + 'invalidstr02': ('{{ var|default_if_none:"Foo" }}', {}, ('','INVALID')), + 'invalidstr03': ('{% for v in var %}({{ v }}){% endfor %}', {}, ''), + 'invalidstr04': ('{% if var %}Yes{% else %}No{% endif %}', {}, 'No'), + 'invalidstr04': ('{% if var|default:"Foo" %}Yes{% else %}No{% endif %}', {}, 'Yes'), + ### MULTILINE ############################################################# 'multiline01': (""" @@ -507,7 +515,7 @@ class Templates(unittest.TestCase): '{{ item.foo }}' + \ '{% endfor %},' + \ '{% endfor %}', - {}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'), + {}, ''), ### TEMPLATETAG TAG ####################################################### 'templatetag01': ('{% templatetag openblock %}', {}, '{%'), @@ -592,30 +600,44 @@ class Templates(unittest.TestCase): old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False # Set TEMPLATE_STRING_IF_INVALID to a known string - old_invalid, settings.TEMPLATE_STRING_IF_INVALID = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID' + old_invalid = settings.TEMPLATE_STRING_IF_INVALID for name, vals in tests: install() + + if isinstance(vals[2], tuple): + normal_string_result = vals[2][0] + invalid_string_result = vals[2][1] + else: + normal_string_result = vals[2] + invalid_string_result = vals[2] + if 'LANGUAGE_CODE' in vals[1]: activate(vals[1]['LANGUAGE_CODE']) else: activate('en-us') - try: - output = loader.get_template(name).render(template.Context(vals[1])) - except Exception, e: - if e.__class__ != vals[2]: - failures.append("Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e)) - continue + + for invalid_str, result in [('', normal_string_result), + ('INVALID', invalid_string_result)]: + settings.TEMPLATE_STRING_IF_INVALID = invalid_str + try: + output = loader.get_template(name).render(template.Context(vals[1])) + except Exception, e: + if e.__class__ != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e)) + continue + if output != result: + failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output)) + if 'LANGUAGE_CODE' in vals[1]: deactivate() - if output != vals[2]: - failures.append("Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output)) + loader.template_source_loaders = old_template_loaders deactivate() settings.TEMPLATE_DEBUG = old_td settings.TEMPLATE_STRING_IF_INVALID = old_invalid - self.assertEqual(failures, []) + self.assertEqual(failures, [], '\n'.join(failures)) if __name__ == "__main__": unittest.main() diff --git a/tests/regressiontests/thread_isolation/tests.py b/tests/regressiontests/thread_isolation/tests.py index f677c27bf8..39c141720a 100644 --- a/tests/regressiontests/thread_isolation/tests.py +++ b/tests/regressiontests/thread_isolation/tests.py @@ -34,8 +34,9 @@ except ImportError: # Import copy of _thread_local.py from Python 2.4 from django.utils._threading_local import local - # helpers +EV = threading.Event() + class LocalSettings: """Settings holder that allows thread-local overrides of defaults. """ @@ -69,7 +70,7 @@ def thread_two(func, *arg): debug("t2 ODB: %s", settings.OTHER_DATABASES) debug("t2 waiting") - ev.wait(2.0) + EV.wait(2.0) func(*arg) debug("t2 complete") t2 = threading.Thread(target=start) @@ -94,7 +95,7 @@ def thread_three(func, *arg): connection.settings.DATABASE_NAME) debug("t3 waiting") - ev.wait(2.0) + EV.wait(2.0) func(*arg) debug("t3 complete") t3 = threading.Thread(target=start) @@ -113,7 +114,6 @@ def start_response(code, headers): class TestThreadIsolation(unittest.TestCase): # event used to synchronize threads so we can be sure they are running # together - ev = threading.Event() lock = threading.RLock() errors = [] @@ -235,7 +235,7 @@ class TestThreadIsolation(unittest.TestCase): t3 = thread_three(MockHandler(self.request_three), env, start_response) try: - self.ev.set() + EV.set() MockHandler(self.request_one)(env, start_response) finally: t2.join() diff --git a/tests/runtests.py b/tests/runtests.py index a8dd08e898..20cb446929 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -6,6 +6,7 @@ import unittest MODEL_TESTS_DIR_NAME = 'modeltests' REGRESSION_TESTS_DIR_NAME = 'regressiontests' TEST_DATABASE_NAME = 'django_test_db' +TEST_TEMPLATE_DIR = 'templates' TEST_DATABASES = ('_a', '_b') @@ -81,20 +82,25 @@ class InvalidModelTestCase(unittest.TestCase): def django_tests(verbosity, tests_to_run): from django.conf import settings from django.db.models.loading import get_apps, load_app + old_installed_apps = settings.INSTALLED_APPS old_test_database_name = settings.TEST_DATABASE_NAME + old_root_urlconf = settings.ROOT_URLCONF + old_template_dirs = settings.TEMPLATE_DIRS + # Redirect some settings for the duration of these tests settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS - settings.TEST_DATABASES = TEST_DATABASES settings.TEST_DATABASE_MODELS = TEST_DATABASE_MODELS + settings.ROOT_URLCONF = 'urls' + settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),) # load all the ALWAYS_INSTALLED_APPS get_apps() - test_models = [] # Load all the test model apps + test_models = [] for model_dir, model_name in get_test_models(): model_label = '.'.join([model_dir, model_name]) try: @@ -125,6 +131,9 @@ def django_tests(verbosity, tests_to_run): # Restore the old settings settings.INSTALLED_APPS = old_installed_apps settings.TESTS_DATABASE_NAME = old_test_database_name + settings.ROOT_URLCONF = old_root_urlconf + settings.TEMPLATE_DIRS = old_template_dirs + if __name__ == "__main__": from optparse import OptionParser usage = "%prog [options] [model model model ...]" diff --git a/tests/templates/404.html b/tests/templates/404.html new file mode 100644 index 0000000000..da627e2222 --- /dev/null +++ b/tests/templates/404.html @@ -0,0 +1 @@ +Django Internal Tests: 404 Error
\ No newline at end of file diff --git a/tests/templates/500.html b/tests/templates/500.html new file mode 100644 index 0000000000..ff028cbeb0 --- /dev/null +++ b/tests/templates/500.html @@ -0,0 +1 @@ +Django Internal Tests: 500 Error
\ No newline at end of file diff --git a/tests/templates/login.html b/tests/templates/login.html new file mode 100644 index 0000000000..8a0974c9a1 --- /dev/null +++ b/tests/templates/login.html @@ -0,0 +1,19 @@ +<html> +<head></head> +<body> +<h1>Django Internal Tests: Login</h1> +{% if form.has_errors %} +<p>Your username and password didn't match. Please try again.</p> +{% endif %} + +<form method="post" action="."> +<table> +<tr><td><label for="id_username">Username:</label></td><td>{{ form.username }}</td></tr> +<tr><td><label for="id_password">Password:</label></td><td>{{ form.password }}</td></tr> +</table> + +<input type="submit" value="login" /> +<input type="hidden" name="next" value="{{ next }}" /> +</form> +</body> +</html>
\ No newline at end of file diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000000..39d5aaee6b --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + # test_client modeltest urls + (r'^test_client/', include('modeltests.test_client.urls')), + + # Always provide the auth system login and logout views + (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), + (r'^accounts/logout/$', 'django.contrib.auth.views.login'), +) |
