diff options
Diffstat (limited to 'django/contrib')
57 files changed, 1523 insertions, 841 deletions
diff --git a/django/contrib/admin/media/css/base.css b/django/contrib/admin/media/css/base.css index da502f357a..9c1f71f810 100644 --- a/django/contrib/admin/media/css/base.css +++ b/django/contrib/admin/media/css/base.css @@ -445,6 +445,14 @@ ul.messagelist li { background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; } +ul.messagelist li.warning{ + background-image: url(../img/admin/icon_alert.gif); +} + +ul.messagelist li.error{ + background-image: url(../img/admin/icon_error.gif); +} + .errornote { font-size: 12px !important; display: block; @@ -470,6 +478,11 @@ ul.errorlist { background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat; } +.errorlist li a { + color: white; + text-decoration: underline; +} + td ul.errorlist { margin: 0 !important; padding: 0 !important; diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 99ff8bc0cf..282833c440 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -9,6 +9,8 @@ width: 100%; } +.change-list .hiddenfields { display:none; } + .change-list .filtered table { border-right: 1px solid #ddd; } @@ -21,7 +23,7 @@ background: white url(../img/admin/changelist-bg.gif) top right repeat-y !important; } -.change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { +.change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { margin-right: 160px !important; width: auto !important; } diff --git a/django/contrib/admin/media/js/dateparse.js b/django/contrib/admin/media/js/dateparse.js index e1c870e146..3cb82dea13 100644 --- a/django/contrib/admin/media/js/dateparse.js +++ b/django/contrib/admin/media/js/dateparse.js @@ -100,8 +100,9 @@ var dateParsePatterns = [ { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, handler: function(bits) { var d = new Date(); - d.setDate(parseInt(bits[1], 10)); + d.setDate(1); d.setMonth(parseMonth(bits[2])); + d.setDate(parseInt(bits[1], 10)); return d; } }, @@ -109,9 +110,10 @@ var dateParsePatterns = [ { re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i, handler: function(bits) { var d = new Date(); - d.setDate(parseInt(bits[1], 10)); - d.setMonth(parseMonth(bits[2])); + d.setDate(1); d.setYear(bits[3]); + d.setMonth(parseMonth(bits[2])); + d.setDate(parseInt(bits[1], 10)); return d; } }, @@ -119,8 +121,9 @@ var dateParsePatterns = [ { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i, handler: function(bits) { var d = new Date(); - d.setDate(parseInt(bits[2], 10)); + d.setDate(1); d.setMonth(parseMonth(bits[1])); + d.setDate(parseInt(bits[2], 10)); return d; } }, @@ -128,9 +131,10 @@ var dateParsePatterns = [ { re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i, handler: function(bits) { var d = new Date(); - d.setDate(parseInt(bits[2], 10)); - d.setMonth(parseMonth(bits[1])); + d.setDate(1); d.setYear(bits[3]); + d.setMonth(parseMonth(bits[1])); + d.setDate(parseInt(bits[2], 10)); return d; } }, @@ -158,9 +162,10 @@ var dateParsePatterns = [ { re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/, handler: function(bits) { var d = new Date(); + d.setDate(1); d.setYear(bits[3]); - d.setDate(parseInt(bits[2], 10)); d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0 + d.setDate(parseInt(bits[2], 10)); return d; } }, @@ -168,6 +173,7 @@ var dateParsePatterns = [ { re: /(\d{4})-(\d{1,2})-(\d{1,2})/, handler: function(bits) { var d = new Date(); + d.setDate(1); d.setYear(parseInt(bits[1])); d.setMonth(parseInt(bits[2], 10) - 1); d.setDate(parseInt(bits[3], 10)); diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index abbe14c948..ffa3a533e9 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -108,7 +108,13 @@ class BaseModelAdmin(object): # rendered output. formfield can be None if it came from a # OneToOneField with parent_link=True or a M2M intermediary. if formfield and db_field.name not in self.raw_id_fields: - formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) + related_modeladmin = self.admin_site._registry.get( + db_field.rel.to) + can_add_related = bool(related_modeladmin and + related_modeladmin.has_add_permission(request)) + formfield.widget = widgets.RelatedFieldWidgetWrapper( + formfield.widget, db_field.rel, self.admin_site, + can_add_related=can_add_related) return formfield @@ -502,7 +508,7 @@ class ModelAdmin(BaseModelAdmin): # Convert the actions into a SortedDict keyed by name # and sorted by description. - actions.sort(lambda a,b: cmp(a[2].lower(), b[2].lower())) + actions.sort(key=lambda k: k[2].lower()) actions = SortedDict([ (name, (func, name, desc)) for func, name, desc in actions @@ -755,7 +761,7 @@ class ModelAdmin(BaseModelAdmin): if isinstance(response, HttpResponse): return response else: - return HttpResponseRedirect(".") + return HttpResponseRedirect(request.get_full_path()) else: msg = _("No action selected.") self.message_user(request, msg) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index f4b6953347..ddc4f8c84b 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -380,11 +380,11 @@ class AdminSite(object): # Sort the apps alphabetically. app_list = app_dict.values() - app_list.sort(lambda x, y: cmp(x['name'], y['name'])) + app_list.sort(key=lambda x: x['name']) # Sort the models alphabetically within each app. for app in app_list: - app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) + app['models'].sort(key=lambda x: x['name']) context = { 'title': _('Site administration'), @@ -445,7 +445,7 @@ class AdminSite(object): if not app_dict: raise http.Http404('The requested admin page does not exist.') # Sort the models alphabetically within each app. - app_dict['models'].sort(lambda x, y: cmp(x['name'], y['name'])) + app_dict['models'].sort(key=lambda x: x['name']) context = { 'title': _('%s administration') % app_instance.verbose_name, 'app_list': [app_dict], diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html index b57f59b82e..c8889eb069 100644 --- a/django/contrib/admin/templates/admin/auth/user/add_form.html +++ b/django/contrib/admin/templates/admin/auth/user/add_form.html @@ -2,8 +2,11 @@ {% load i18n %} {% block form_top %} + {% if not is_popup %} <p>{% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}</p> - <input type="hidden" name="_continue" value="1" /> + {% else %} + <p>{% trans "Enter a username and password." %}</p> + {% endif %} {% endblock %} {% block after_field_sets %} diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 3fd9eb3770..4221e2d1a7 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -56,7 +56,9 @@ {% endif %} {% if messages %} - <ul class="messagelist">{% for message in messages %}<li>{{ message }}</li>{% endfor %}</ul> + <ul class="messagelist">{% for message in messages %} + <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li> + {% endfor %}</ul> {% endif %} <!-- Content --> diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html index 0efcc9b24a..6d4de2df0e 100644 --- a/django/contrib/admin/templates/admin/change_list_results.html +++ b/django/contrib/admin/templates/admin/change_list_results.html @@ -1,4 +1,10 @@ +{% if result_hidden_fields %} +<div class="hiddenfields"> {# DIV for HTML validation #} +{% for item in result_hidden_fields %}{{ item }}{% endfor %} +</div> +{% endif %} {% if results %} +<div class="results"> <table cellspacing="0" id="result_list"> <thead> <tr> @@ -14,4 +20,5 @@ {% endfor %} </tbody> </table> +</div> {% endif %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 565db32251..c05af0b7fe 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -189,7 +189,7 @@ def items_for_result(cl, result, form): else: result_repr = conditional_escape(result_repr) yield mark_safe(u'<td%s>%s</td>' % (row_class, result_repr)) - if form: + if form and not form[cl.model._meta.pk.name].is_hidden: yield mark_safe(u'<td>%s</td>' % force_unicode(form[cl.model._meta.pk.name])) def results(cl): @@ -200,11 +200,18 @@ def results(cl): for res in cl.result_list: yield list(items_for_result(cl, res, None)) +def result_hidden_fields(cl): + if cl.formset: + for res, form in zip(cl.result_list, cl.formset.forms): + if form[cl.model._meta.pk.name].is_hidden: + yield mark_safe(force_unicode(form[cl.model._meta.pk.name])) + def result_list(cl): """ Displays the headers and data list together """ return {'cl': cl, + 'result_hidden_fields': list(result_hidden_fields(cl)), 'result_headers': list(result_headers(cl)), 'results': list(results(cl))} result_list = register.inclusion_tag("admin/change_list_results.html")(result_list) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 1d321d0620..2c7ac5c794 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -154,9 +154,9 @@ class ForeignKeyRawIdWidget(forms.TextInput): key = self.rel.get_related_field().name try: obj = self.rel.to._default_manager.using(self.db).get(**{key: value}) - except self.rel.to.DoesNotExist: + return ' <strong>%s</strong>' % escape(truncate_words(obj, 14)) + except (ValueError, self.rel.to.DoesNotExist): return '' - return ' <strong>%s</strong>' % escape(truncate_words(obj, 14)) class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): """ @@ -169,7 +169,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): def render(self, name, value, attrs=None): attrs['class'] = 'vManyToManyRawIdAdminField' if value: - value = ','.join([str(v) for v in value]) + value = ','.join([force_unicode(v) for v in value]) else: value = '' return super(ManyToManyRawIdWidget, self).render(name, value, attrs) @@ -205,13 +205,18 @@ class RelatedFieldWidgetWrapper(forms.Widget): This class is a wrapper to a given widget to add the add icon for the admin interface. """ - def __init__(self, widget, rel, admin_site): + def __init__(self, widget, rel, admin_site, can_add_related=None): self.is_hidden = widget.is_hidden self.needs_multipart_form = widget.needs_multipart_form self.attrs = widget.attrs self.choices = widget.choices self.widget = widget self.rel = rel + # Backwards compatible check for whether a user can add related + # objects. + if can_add_related is None: + can_add_related = rel_to in self.admin_site._registry + self.can_add_related = can_add_related # so we can check if the related object is registered with this AdminSite self.admin_site = admin_site @@ -236,7 +241,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): related_url = '%s%s/%s/add/' % info self.widget.choices = self.choices output = [self.widget.render(name, value, *args, **kwargs)] - if rel_to in self.admin_site._registry: # If the related object has an admin interface: + if self.can_add_related: # TODO: "id_" is hard-coded here. This should instead use the correct # API to determine the ID dynamically. output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \ diff --git a/django/contrib/admindocs/templates/admin_doc/template_filter_index.html b/django/contrib/admindocs/templates/admin_doc/template_filter_index.html index 53383edc23..7470762775 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_filter_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_filter_index.html @@ -15,7 +15,7 @@ <h2>{% firstof library.grouper "Built-in filters" %}</h2> {% if library.grouper %}<p class="small quiet">To use these filters, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the filter.</p><hr />{% endif %} {% for filter in library.list|dictsort:"name" %} - <h3 id="{{ filter.name }}">{{ filter.name }}</h3> + <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3> <p>{{ filter.title }}</p> <p>{{ filter.body }}</p> {% if not forloop.last %}<hr />{% endif %} @@ -36,7 +36,7 @@ <h2>{% firstof library.grouper "Built-in filters" %}</h2> <ul> {% for filter in library.list|dictsort:"name" %} - <li><a href="#{{ filter.name }}">{{ filter.name }}</a></li> + <li><a href="#{{ library.grouper|default_if_none:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li> {% endfor %} </ul> </div> diff --git a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html index a4ad66fd96..774130bd70 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html @@ -15,7 +15,7 @@ <h2>{% firstof library.grouper "Built-in tags" %}</h2> {% if library.grouper %}<p class="small quiet">To use these tags, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the tag.</p><hr />{% endif %} {% for tag in library.list|dictsort:"name" %} - <h3 id="{{ tag.name }}">{{ tag.name }}</h3> + <h3 id="{{ library.grouper|default_if_none:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3> <h4>{{ tag.title }}</h4> <p>{{ tag.body }}</p> {% if not forloop.last %}<hr />{% endif %} @@ -36,7 +36,7 @@ <h2>{% firstof library.grouper "Built-in tags" %}</h2> <ul> {% for tag in library.list|dictsort:"name" %} - <li><a href="#{{ tag.name }}">{{ tag.name }}</a></li> + <li><a href="#{{ library.grouper|default_if_none:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li> {% endfor %} </ul> </div> diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index e154c9299a..5bfa0f7184 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -54,7 +54,9 @@ def template_tag_index(request): load_all_installed_template_libraries() tags = [] - for module_name, library in template.libraries.items(): + app_libs = template.libraries.items() + builtin_libs = [(None, lib) for lib in template.builtins] + for module_name, library in builtin_libs + app_libs: for tag_name, tag_func in library.tags.items(): title, body, metadata = utils.parse_docstring(tag_func.__doc__) if title: @@ -87,7 +89,9 @@ def template_filter_index(request): load_all_installed_template_libraries() filters = [] - for module_name, library in template.libraries.items(): + app_libs = template.libraries.items() + builtin_libs = [(None, lib) for lib in template.builtins] + for module_name, library in builtin_libs + app_libs: for filter_name, filter_func in library.filters.items(): title, body, metadata = utils.parse_docstring(filter_func.__doc__) if title: diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 09dcf42e42..7d7a0cddb7 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -30,13 +30,14 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE return decorator -def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME): +def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): """ Decorator for views that checks that the user is logged in, redirecting to the log-in page if necessary. """ actual_decorator = user_passes_test( lambda u: u.is_authenticated(), + login_url=login_url, redirect_field_name=redirect_field_name ) if function: diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 086acf3349..cc0f7071c3 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -52,6 +52,12 @@ class UserChangeForm(forms.ModelForm): class Meta: model = User + def __init__(self, *args, **kwargs): + super(UserChangeForm, self).__init__(*args, **kwargs) + f = self.fields.get('user_permissions', None) + if f is not None: + f.queryset = f.queryset.select_related('content_type') + class AuthenticationForm(forms.Form): """ Base class for authenticating users. Extend this to get a form that accepts @@ -111,7 +117,7 @@ class PasswordResetForm(forms.Form): return email def save(self, domain_override=None, email_template_name='registration/password_reset_email.html', - use_https=False, token_generator=default_token_generator): + use_https=False, token_generator=default_token_generator, from_email=None): """ Generates a one-use only link for resetting password and sends to the user """ @@ -134,7 +140,7 @@ class PasswordResetForm(forms.Form): 'protocol': use_https and 'https' or 'http', } send_mail(_("Password reset on %s") % site_name, - t.render(Context(c)), None, [user.email]) + t.render(Context(c)), from_email, [user.email]) class SetPasswordForm(forms.Form): """ diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 965ea2d6be..a1d02b6014 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -1,7 +1,7 @@ from django.contrib.auth.tests.auth_backends import BackendTest, RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest from django.contrib.auth.tests.basic import BASIC_TESTS from django.contrib.auth.tests.decorators import LoginRequiredTestCase -from django.contrib.auth.tests.forms import FORM_TESTS +from django.contrib.auth.tests.forms import UserCreationFormTest, AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, UserChangeFormTest, PasswordResetFormTest from django.contrib.auth.tests.remote_user \ import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest from django.contrib.auth.tests.models import ProfileTestCase @@ -13,6 +13,5 @@ from django.contrib.auth.tests.views \ __test__ = { 'BASIC_TESTS': BASIC_TESTS, - 'FORM_TESTS': FORM_TESTS, 'TOKEN_GENERATOR_TESTS': TOKEN_GENERATOR_TESTS, } diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index 7efd9d8ccf..0240a76eb7 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -1,12 +1,12 @@ -from unittest import TestCase - from django.contrib.auth.decorators import login_required +from django.contrib.auth.tests.views import AuthViewsTestCase - -class LoginRequiredTestCase(TestCase): +class LoginRequiredTestCase(AuthViewsTestCase): """ Tests the login_required decorators """ + urls = 'django.contrib.auth.tests.urls' + def testCallable(self): """ Check that login_required is assignable to callable objects. @@ -22,4 +22,24 @@ class LoginRequiredTestCase(TestCase): """ def normal_view(request): pass - login_required(normal_view)
\ No newline at end of file + login_required(normal_view) + + def testLoginRequired(self, view_url='/login_required/', login_url='/login/'): + """ + Check that login_required works on a simple view wrapped in a + login_required decorator. + """ + response = self.client.get(view_url) + self.assertEqual(response.status_code, 302) + self.assert_(login_url in response['Location']) + self.login() + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + + def testLoginRequiredNextUrl(self): + """ + Check that login_required works on a simple view wrapped in a + login_required decorator with a login_url set. + """ + self.testLoginRequired(view_url='/login_required_login_url/', + login_url='/somewhere/')
\ No newline at end of file diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index b691c560be..5aa49e09c3 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -1,231 +1,252 @@ - -FORM_TESTS = """ ->>> from django.contrib.auth.models import User ->>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm ->>> from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm - -# The user already exists. - ->>> user = User.objects.create_user("jsmith", "jsmith@example.com", "test123") ->>> data = { -... 'username': 'jsmith', -... 'password1': 'test123', -... 'password2': 'test123', -... } ->>> form = UserCreationForm(data) ->>> form.is_valid() -False ->>> form["username"].errors -[u'A user with that username already exists.'] - -# The username contains invalid data. - ->>> data = { -... 'username': 'jsmith!', -... 'password1': 'test123', -... 'password2': 'test123', -... } ->>> form = UserCreationForm(data) ->>> form.is_valid() -False ->>> form["username"].errors -[u'This value may contain only letters, numbers and @/./+/-/_ characters.'] - -# The verification password is incorrect. - ->>> data = { -... 'username': 'jsmith2', -... 'password1': 'test123', -... 'password2': 'test', -... } ->>> form = UserCreationForm(data) ->>> form.is_valid() -False ->>> form["password2"].errors -[u"The two password fields didn't match."] - -# One (or both) passwords weren't given - ->>> data = {'username': 'jsmith2'} ->>> form = UserCreationForm(data) ->>> form.is_valid() -False ->>> form['password1'].errors -[u'This field is required.'] ->>> form['password2'].errors -[u'This field is required.'] - ->>> data['password2'] = 'test123' ->>> form = UserCreationForm(data) ->>> form.is_valid() -False ->>> form['password1'].errors -[u'This field is required.'] - -# The success case. - ->>> data = { -... 'username': 'jsmith2@example.com', -... 'password1': 'test123', -... 'password2': 'test123', -... } ->>> form = UserCreationForm(data) ->>> form.is_valid() -True ->>> form.save() -<User: jsmith2@example.com> - -# The user submits an invalid username. - ->>> data = { -... 'username': 'jsmith_does_not_exist', -... 'password': 'test123', -... } - ->>> form = AuthenticationForm(None, data) ->>> form.is_valid() -False ->>> form.non_field_errors() -[u'Please enter a correct username and password. Note that both fields are case-sensitive.'] - -# The user is inactive. - ->>> data = { -... 'username': 'jsmith', -... 'password': 'test123', -... } ->>> user.is_active = False ->>> user.save() ->>> form = AuthenticationForm(None, data) ->>> form.is_valid() -False ->>> form.non_field_errors() -[u'This account is inactive.'] - ->>> user.is_active = True ->>> user.save() - -# The success case - ->>> form = AuthenticationForm(None, data) ->>> form.is_valid() -True ->>> form.non_field_errors() -[] - -### SetPasswordForm: - -# The two new passwords do not match. - ->>> data = { -... 'new_password1': 'abc123', -... 'new_password2': 'abc', -... } ->>> form = SetPasswordForm(user, data) ->>> form.is_valid() -False ->>> form["new_password2"].errors -[u"The two password fields didn't match."] - -# The success case. - ->>> data = { -... 'new_password1': 'abc123', -... 'new_password2': 'abc123', -... } ->>> form = SetPasswordForm(user, data) ->>> form.is_valid() -True - -### PasswordChangeForm: - -The old password is incorrect. - ->>> data = { -... 'old_password': 'test', -... 'new_password1': 'abc123', -... 'new_password2': 'abc123', -... } ->>> form = PasswordChangeForm(user, data) ->>> form.is_valid() -False ->>> form["old_password"].errors -[u'Your old password was entered incorrectly. Please enter it again.'] - -# The two new passwords do not match. - ->>> data = { -... 'old_password': 'test123', -... 'new_password1': 'abc123', -... 'new_password2': 'abc', -... } ->>> form = PasswordChangeForm(user, data) ->>> form.is_valid() -False ->>> form["new_password2"].errors -[u"The two password fields didn't match."] - -# The success case. - ->>> data = { -... 'old_password': 'test123', -... 'new_password1': 'abc123', -... 'new_password2': 'abc123', -... } ->>> form = PasswordChangeForm(user, data) ->>> form.is_valid() -True - -# Regression test - check the order of fields: - ->>> PasswordChangeForm(user, {}).fields.keys() -['old_password', 'new_password1', 'new_password2'] - -### UserChangeForm - ->>> from django.contrib.auth.forms import UserChangeForm ->>> data = {'username': 'not valid'} ->>> form = UserChangeForm(data, instance=user) ->>> form.is_valid() -False ->>> form['username'].errors -[u'This value may contain only letters, numbers and @/./+/-/_ characters.'] - - -### PasswordResetForm - ->>> from django.contrib.auth.forms import PasswordResetForm ->>> data = {'email':'not valid'} ->>> form = PasswordResetForm(data) ->>> form.is_valid() -False ->>> form['email'].errors -[u'Enter a valid e-mail address.'] - -# Test nonexistant email address ->>> data = {'email':'foo@bar.com'} ->>> form = PasswordResetForm(data) ->>> form.is_valid() -False ->>> form.errors -{'email': [u"That e-mail address doesn't have an associated user account. Are you sure you've registered?"]} - -# Test cleaned_data bug fix ->>> user = User.objects.create_user("jsmith3", "jsmith3@example.com", "test123") ->>> data = {'email':'jsmith3@example.com'} ->>> form = PasswordResetForm(data) ->>> form.is_valid() -True ->>> form.cleaned_data['email'] -u'jsmith3@example.com' - -# bug #5605, preserve the case of the user name (before the @ in the email address) -# when creating a user. ->>> user = User.objects.create_user('forms_test2', 'tesT@EXAMple.com', 'test') ->>> user.email -'tesT@example.com' ->>> user = User.objects.create_user('forms_test3', 'tesT', 'test') ->>> user.email -'tesT' - -""" +from django.contrib.auth.models import User +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm +from django.test import TestCase + + +class UserCreationFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_user_already_exists(self): + data = { + 'username': 'testclient', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["username"].errors, + [u'A user with that username already exists.']) + + def test_invalid_data(self): + data = { + 'username': 'jsmith!', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["username"].errors, + [u'This value may contain only letters, numbers and @/./+/-/_ characters.']) + + + def test_password_verification(self): + # The verification password is incorrect. + data = { + 'username': 'jsmith', + 'password1': 'test123', + 'password2': 'test', + } + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["password2"].errors, + [u"The two password fields didn't match."]) + + + def test_both_passwords(self): + # One (or both) passwords weren't given + data = {'username': 'jsmith'} + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form['password1'].errors, + [u'This field is required.']) + self.assertEqual(form['password2'].errors, + [u'This field is required.']) + + + data['password2'] = 'test123' + form = UserCreationForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form['password1'].errors, + [u'This field is required.']) + + def test_success(self): + # The success case. + + data = { + 'username': 'jsmith@example.com', + 'password1': 'test123', + 'password2': 'test123', + } + form = UserCreationForm(data) + self.assertTrue(form.is_valid()) + u = form.save() + self.assertEqual(repr(u), '<User: jsmith@example.com>') + + +class AuthenticationFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_invalid_username(self): + # The user submits an invalid username. + + data = { + 'username': 'jsmith_does_not_exist', + 'password': 'test123', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [u'Please enter a correct username and password. Note that both fields are case-sensitive.']) + + def test_inactive_user(self): + # The user is inactive. + data = { + 'username': 'inactive', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), + [u'This account is inactive.']) + + + def test_success(self): + # The success case + data = { + 'username': 'testclient', + 'password': 'password', + } + form = AuthenticationForm(None, data) + self.assertTrue(form.is_valid()) + self.assertEqual(form.non_field_errors(), []) + + +class SetPasswordFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_password_verification(self): + # The two new passwords do not match. + user = User.objects.get(username='testclient') + data = { + 'new_password1': 'abc123', + 'new_password2': 'abc', + } + form = SetPasswordForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["new_password2"].errors, + [u"The two password fields didn't match."]) + + def test_success(self): + user = User.objects.get(username='testclient') + data = { + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = SetPasswordForm(user, data) + self.assertTrue(form.is_valid()) + + +class PasswordChangeFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_incorrect_password(self): + user = User.objects.get(username='testclient') + data = { + 'old_password': 'test', + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = PasswordChangeForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["old_password"].errors, + [u'Your old password was entered incorrectly. Please enter it again.']) + + + def test_password_verification(self): + # The two new passwords do not match. + user = User.objects.get(username='testclient') + data = { + 'old_password': 'password', + 'new_password1': 'abc123', + 'new_password2': 'abc', + } + form = PasswordChangeForm(user, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form["new_password2"].errors, + [u"The two password fields didn't match."]) + + + def test_success(self): + # The success case. + user = User.objects.get(username='testclient') + data = { + 'old_password': 'password', + 'new_password1': 'abc123', + 'new_password2': 'abc123', + } + form = PasswordChangeForm(user, data) + self.assertTrue(form.is_valid()) + + def test_field_order(self): + # Regression test - check the order of fields: + user = User.objects.get(username='testclient') + self.assertEqual(PasswordChangeForm(user, {}).fields.keys(), + ['old_password', 'new_password1', 'new_password2']) + +class UserChangeFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_username_validity(self): + user = User.objects.get(username='testclient') + data = {'username': 'not valid'} + form = UserChangeForm(data, instance=user) + self.assertFalse(form.is_valid()) + self.assertEqual(form['username'].errors, + [u'This value may contain only letters, numbers and @/./+/-/_ characters.']) + + def test_bug_14242(self): + # A regression test, introduce by adding an optimization for the + # UserChangeForm. + + class MyUserForm(UserChangeForm): + def __init__(self, *args, **kwargs): + super(MyUserForm, self).__init__(*args, **kwargs) + self.fields['groups'].help_text = 'These groups give users different permissions' + + class Meta(UserChangeForm.Meta): + fields = ('groups',) + + # Just check we can create it + form = MyUserForm({}) + + +class PasswordResetFormTest(TestCase): + + fixtures = ['authtestdata.json'] + + def test_invalid_email(self): + data = {'email':'not valid'} + form = PasswordResetForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form['email'].errors, + [u'Enter a valid e-mail address.']) + + def test_nonexistant_email(self): + # Test nonexistant email address + data = {'email':'foo@bar.com'} + form = PasswordResetForm(data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors, + {'email': [u"That e-mail address doesn't have an associated user account. Are you sure you've registered?"]}) + + def test_cleaned_data(self): + # Regression test + user = User.objects.create_user("jsmith3", "jsmith3@example.com", "test123") + data = {'email':'jsmith3@example.com'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['email'], u'jsmith3@example.com') + + + def test_bug_5605(self): + # bug #5605, preserve the case of the user name (before the @ in the + # email address) when creating a user. + user = User.objects.create_user('forms_test2', 'tesT@EXAMple.com', 'test') + self.assertEqual(user.email, 'tesT@example.com') + user = User.objects.create_user('forms_test3', 'tesT', 'test') + self.assertEqual(user.email, 'tesT') diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index f94b8daa7f..bc2f7e705f 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -1,5 +1,7 @@ from django.conf.urls.defaults import patterns from django.contrib.auth.urls import urlpatterns +from django.contrib.auth.views import password_reset +from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.template import Template, RequestContext @@ -14,5 +16,8 @@ urlpatterns += patterns('', (r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')), (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), (r'^remote_user/$', remote_user_auth_view), + (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), + (r'^login_required/$', login_required(password_reset)), + (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), ) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index d894e6dafd..e20afbc5b0 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -36,6 +36,16 @@ class AuthViewsTestCase(TestCase): settings.LANGUAGE_CODE = self.old_LANGUAGE_CODE settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + def login(self, password='password'): + response = self.client.post('/login/', { + 'username': 'testclient', + 'password': password + } + ) + self.assertEquals(response.status_code, 302) + self.assert_(response['Location'].endswith(settings.LOGIN_REDIRECT_URL)) + self.assert_(SESSION_KEY in self.client.session) + class PasswordResetTest(AuthViewsTestCase): def test_email_not_found(self): @@ -52,6 +62,14 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEquals(response.status_code, 302) self.assertEquals(len(mail.outbox), 1) self.assert_("http://" in mail.outbox[0].body) + self.assertEquals(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + + def test_email_found_custom_from(self): + "Email is sent if a valid email address is provided for password reset when a custom from_email is provided." + response = self.client.post('/password_reset_from_email/', {'email': 'staffmember@example.com'}) + self.assertEquals(response.status_code, 302) + self.assertEquals(len(mail.outbox), 1) + self.assertEquals("staffmember@example.com", mail.outbox[0].from_email) def _test_confirm_start(self): # Start by creating the email @@ -118,15 +136,6 @@ class PasswordResetTest(AuthViewsTestCase): class ChangePasswordTest(AuthViewsTestCase): - def login(self, password='password'): - response = self.client.post('/login/', { - 'username': 'testclient', - 'password': password - } - ) - self.assertEquals(response.status_code, 302) - self.assert_(response['Location'].endswith(settings.LOGIN_REDIRECT_URL)) - def fail_login(self, password='password'): response = self.client.post('/login/', { 'username': 'testclient', @@ -228,16 +237,6 @@ class LoginTest(AuthViewsTestCase): class LogoutTest(AuthViewsTestCase): urls = 'django.contrib.auth.tests.urls' - def login(self, password='password'): - response = self.client.post('/login/', { - 'username': 'testclient', - 'password': password - } - ) - self.assertEquals(response.status_code, 302) - self.assert_(response['Location'].endswith(settings.LOGIN_REDIRECT_URL)) - self.assert_(SESSION_KEY in self.client.session) - def confirm_logged_out(self): self.assert_(SESSION_KEY not in self.client.session) diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index b2e875a869..eaa0dbeba2 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -105,7 +105,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', password_reset_form=PasswordResetForm, token_generator=default_token_generator, - post_reset_redirect=None): + post_reset_redirect=None, from_email=None): if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done') if request.method == "POST": @@ -114,6 +114,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas opts = {} opts['use_https'] = request.is_secure() opts['token_generator'] = token_generator + opts['from_email'] = from_email if is_admin_site: opts['domain_override'] = request.META['HTTP_HOST'] else: diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py index 9bbd02da26..8a08cfd8eb 100644 --- a/django/contrib/databrowse/plugins/calendars.py +++ b/django/contrib/databrowse/plugins/calendars.py @@ -60,7 +60,7 @@ class CalendarPlugin(DatabrowsePlugin): def homepage_view(self, request): easy_model = EasyModel(self.site, self.model) field_list = self.fields.values() - field_list.sort(lambda x, y: cmp(x.verbose_name, y.verbose_name)) + field_list.sort(key=lambda k:k.verbose_name) return render_to_response('databrowse/calendar_homepage.html', {'root_url': self.site.root_url, 'model': easy_model, 'field_list': field_list}) def calendar_view(self, request, field, year=None, month=None, day=None): diff --git a/django/contrib/databrowse/plugins/fieldchoices.py b/django/contrib/databrowse/plugins/fieldchoices.py index 8f77792579..e0c01e95e6 100644 --- a/django/contrib/databrowse/plugins/fieldchoices.py +++ b/django/contrib/databrowse/plugins/fieldchoices.py @@ -61,7 +61,7 @@ class FieldChoicePlugin(DatabrowsePlugin): def homepage_view(self, request): easy_model = EasyModel(self.site, self.model) field_list = self.fields.values() - field_list.sort(lambda x, y: cmp(x.verbose_name, y.verbose_name)) + field_list.sort(key=lambda k: k.verbose_name) return render_to_response('databrowse/fieldchoice_homepage.html', {'root_url': self.site.root_url, 'model': easy_model, 'field_list': field_list}) def field_view(self, request, field, value=None): diff --git a/django/contrib/flatpages/admin.py b/django/contrib/flatpages/admin.py index b6fdba3d53..1b377e967b 100644 --- a/django/contrib/flatpages/admin.py +++ b/django/contrib/flatpages/admin.py @@ -5,11 +5,11 @@ from django.utils.translation import ugettext_lazy as _ class FlatpageForm(forms.ModelForm): - url = forms.RegexField(label=_("URL"), max_length=100, regex=r'^[-\w/]+$', + url = forms.RegexField(label=_("URL"), max_length=100, regex=r'^[-\w/\.~]+$', help_text = _("Example: '/about/contact/'. Make sure to have leading" " and trailing slashes."), error_message = _("This value must contain only letters, numbers," - " underscores, dashes or slashes.")) + " dots, underscores, dashes, slashes or tildes.")) class Meta: model = FlatPage diff --git a/django/contrib/flatpages/fixtures/sample_flatpages.json b/django/contrib/flatpages/fixtures/sample_flatpages.json new file mode 100644 index 0000000000..885af1eb60 --- /dev/null +++ b/django/contrib/flatpages/fixtures/sample_flatpages.json @@ -0,0 +1,63 @@ +[ + { + "pk": 1, + "model": "flatpages.flatpage", + "fields": { + "registration_required": false, + "title": "A Flatpage", + "url": "/flatpage/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it flat!", + "enable_comments": false + } + }, + { + "pk": 2, + "model": "flatpages.flatpage", + "fields": { + "registration_required": false, + "title": "A Nested Flatpage", + "url": "/location/flatpage/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it flat and deep!", + "enable_comments": false + } + }, + + { + "pk": 101, + "model": "flatpages.flatpage", + "fields": { + "registration_required": true, + "title": "Sekrit Flatpage", + "url": "/sekrit/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it sekrit!", + "enable_comments": false + } + }, + { + "pk": 102, + "model": "flatpages.flatpage", + "fields": { + "registration_required": true, + "title": "Sekrit Nested Flatpage", + "url": "/location/sekrit/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it sekrit and deep!", + "enable_comments": false + } + } +]
\ No newline at end of file diff --git a/django/contrib/flatpages/templatetags/__init__.py b/django/contrib/flatpages/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/flatpages/templatetags/__init__.py diff --git a/django/contrib/flatpages/templatetags/flatpages.py b/django/contrib/flatpages/templatetags/flatpages.py new file mode 100644 index 0000000000..5c76699b79 --- /dev/null +++ b/django/contrib/flatpages/templatetags/flatpages.py @@ -0,0 +1,99 @@ +from django import template +from django.contrib.flatpages.models import FlatPage +from django.utils.translation import ugettext as _ +from django.conf import settings + + +register = template.Library() + + +class FlatpageNode(template.Node): + def __init__(self, context_name, starts_with=None, user=None): + self.context_name = context_name + if starts_with: + self.starts_with = template.Variable(starts_with) + else: + self.starts_with = None + if user: + self.user = template.Variable(user) + else: + self.user = None + + def render(self, context): + flatpages = FlatPage.objects.filter(sites__id=settings.SITE_ID) + # If a prefix was specified, add a filter + if self.starts_with: + flatpages = flatpages.filter( + url__startswith=self.starts_with.resolve(context)) + + # If the provided user is not authenticated, or no user + # was provided, filter the list to only public flatpages. + if self.user: + user = self.user.resolve(context) + if not user.is_authenticated(): + flatpages = flatpages.filter(registration_required=False) + else: + flatpages = flatpages.filter(registration_required=False) + + context[self.context_name] = flatpages + return '' + + +def get_flatpages(parser, token): + """ + Retrieves all flatpage objects available for the current site and + visible to the specific user (or visible to all users if no user is + specified). Populates the template context with them in a variable + whose name is defined by the ``as`` clause. + + An optional ``for`` clause can be used to control the user whose + permissions are to be used in determining which flatpages are visible. + + An optional argument, ``starts_with``, can be applied to limit the + returned flatpages to those beginning with a particular base URL. + This argument can be passed as a variable or a string, as it resolves + from the template context. + + Syntax:: + + {% get_flatpages ['url_starts_with'] [for user] as context_name %} + + Example usage:: + + {% get_flatpages as flatpages %} + {% get_flatpages for someuser as flatpages %} + {% get_flatpages '/about/' as about_pages %} + {% get_flatpages prefix as about_pages %} + {% get_flatpages '/about/' for someuser as about_pages %} + """ + bits = token.split_contents() + syntax_message = _("%(tag_name)s expects a syntax of %(tag_name)s " + "['url_starts_with'] [for user] as context_name" % + dict(tag_name=bits[0])) + # Must have at 3-6 bits in the tag + if len(bits) >= 3 and len(bits) <= 6: + + # If there's an even number of bits, there's no prefix + if len(bits) % 2 == 0: + prefix = bits[1] + else: + prefix = None + + # The very last bit must be the context name + if bits[-2] != 'as': + raise template.TemplateSyntaxError(syntax_message) + context_name = bits[-1] + + # If there are 5 or 6 bits, there is a user defined + if len(bits) >= 5: + if bits[-4] != 'for': + raise template.TemplateSyntaxError(syntax_message) + user = bits[-3] + else: + user = None + + return FlatpageNode(context_name, starts_with=prefix, user=user) + else: + raise template.TemplateSyntaxError(syntax_message) + +register.tag('get_flatpages', get_flatpages) diff --git a/django/contrib/flatpages/tests/__init__.py b/django/contrib/flatpages/tests/__init__.py new file mode 100644 index 0000000000..5dd5e89dca --- /dev/null +++ b/django/contrib/flatpages/tests/__init__.py @@ -0,0 +1,5 @@ +from django.contrib.flatpages.tests.csrf import * +from django.contrib.flatpages.tests.forms import * +from django.contrib.flatpages.tests.middleware import * +from django.contrib.flatpages.tests.templatetags import * +from django.contrib.flatpages.tests.views import * diff --git a/django/contrib/flatpages/tests/csrf.py b/django/contrib/flatpages/tests/csrf.py new file mode 100644 index 0000000000..b65ee382a6 --- /dev/null +++ b/django/contrib/flatpages/tests/csrf.py @@ -0,0 +1,76 @@ +import os +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase, Client + +class FlatpageCSRFTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.client = Client(enforce_csrf_checks=True) + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + csrf_middleware_class = 'django.middleware.csrf.CsrfViewMiddleware' + if csrf_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (csrf_middleware_class,) + if flatpage_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (flatpage_middleware_class,) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + + def test_view_flatpage(self): + "A flatpage can be served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it flat!</p>") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + User.objects.create_user('testuser', 'test@example.com', 's3krit') + self.client.login(username='testuser',password='s3krit') + response = self.client.get('/flatpage_root/sekrit/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it sekrit!</p>") + + def test_fallback_flatpage(self): + "A flatpage can be served by the fallback middlware" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it flat!</p>") + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage raises a 404 when served by the fallback middlware" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_post_view_flatpage(self): + "POSTing to a flatpage served through a view will raise a CSRF error if no token is provided (Refs #14156)" + response = self.client.post('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 403) + + def test_post_fallback_flatpage(self): + "POSTing to a flatpage served by the middleware will raise a CSRF error if no token is provided (Refs #14156)" + response = self.client.post('/flatpage/') + self.assertEquals(response.status_code, 403) + + def test_post_unknown_page(self): + "POSTing to an unknown page isn't caught as a 403 CSRF error" + response = self.client.post('/no_such_page/') + self.assertEquals(response.status_code, 404) diff --git a/django/contrib/flatpages/tests/forms.py b/django/contrib/flatpages/tests/forms.py new file mode 100644 index 0000000000..969d347b39 --- /dev/null +++ b/django/contrib/flatpages/tests/forms.py @@ -0,0 +1,22 @@ +from django.contrib.flatpages.admin import FlatpageForm +from django.test import TestCase + +class FlatpageAdminFormTests(TestCase): + def setUp(self): + self.form_data = { + 'title': "A test page", + 'content': "This is a test", + 'sites': [1], + } + + def test_flatpage_admin_form_url_validation(self): + "The flatpage admin form validates correctly validates urls" + self.assertTrue(FlatpageForm(data=dict(url='/new_flatpage/', **self.form_data)).is_valid()) + self.assertTrue(FlatpageForm(data=dict(url='/some.special~chars/', **self.form_data)).is_valid()) + self.assertTrue(FlatpageForm(data=dict(url='/some.very_special~chars-here/', **self.form_data)).is_valid()) + + self.assertFalse(FlatpageForm(data=dict(url='/a space/', **self.form_data)).is_valid()) + self.assertFalse(FlatpageForm(data=dict(url='/a % char/', **self.form_data)).is_valid()) + self.assertFalse(FlatpageForm(data=dict(url='/a ! char/', **self.form_data)).is_valid()) + self.assertFalse(FlatpageForm(data=dict(url='/a & char/', **self.form_data)).is_valid()) + self.assertFalse(FlatpageForm(data=dict(url='/a ? char/', **self.form_data)).is_valid()) diff --git a/django/contrib/flatpages/tests/middleware.py b/django/contrib/flatpages/tests/middleware.py new file mode 100644 index 0000000000..bedaffc8ac --- /dev/null +++ b/django/contrib/flatpages/tests/middleware.py @@ -0,0 +1,67 @@ +import os +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase + +class FlatpageMiddlewareTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + if flatpage_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (flatpage_middleware_class,) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + + def test_view_flatpage(self): + "A flatpage can be served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it flat!</p>") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + User.objects.create_user('testuser', 'test@example.com', 's3krit') + self.client.login(username='testuser',password='s3krit') + response = self.client.get('/flatpage_root/sekrit/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it sekrit!</p>") + + def test_fallback_flatpage(self): + "A flatpage can be served by the fallback middlware" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it flat!</p>") + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage raises a 404 when served by the fallback middlware" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_fallback_authenticated_flatpage(self): + "A flatpage served by the middleware can require authentication" + response = self.client.get('/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/sekrit/') + User.objects.create_user('testuser', 'test@example.com', 's3krit') + self.client.login(username='testuser',password='s3krit') + response = self.client.get('/sekrit/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it sekrit!</p>") diff --git a/django/contrib/flatpages/tests/templates/404.html b/django/contrib/flatpages/tests/templates/404.html new file mode 100644 index 0000000000..5fd5f3cf3b --- /dev/null +++ b/django/contrib/flatpages/tests/templates/404.html @@ -0,0 +1 @@ +<h1>Oh Noes!</h1>
\ No newline at end of file diff --git a/django/contrib/flatpages/tests/templates/flatpages/default.html b/django/contrib/flatpages/tests/templates/flatpages/default.html new file mode 100644 index 0000000000..1410e17adf --- /dev/null +++ b/django/contrib/flatpages/tests/templates/flatpages/default.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" + "http://www.w3.org/TR/REC-html40/loose.dtd"> +<html> +<head> +<title>{{ flatpage.title }}</title> +</head> +<body> +<p>{{ flatpage.content }}</p> +</body> +</html> diff --git a/django/contrib/flatpages/tests/templates/registration/login.html b/django/contrib/flatpages/tests/templates/registration/login.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/flatpages/tests/templates/registration/login.html diff --git a/django/contrib/flatpages/tests/templatetags.py b/django/contrib/flatpages/tests/templatetags.py new file mode 100644 index 0000000000..9f42381c29 --- /dev/null +++ b/django/contrib/flatpages/tests/templatetags.py @@ -0,0 +1,134 @@ +import os +from django.conf import settings +from django.contrib.auth.models import AnonymousUser, User +from django.template import Template, Context, TemplateSyntaxError +from django.test import TestCase + +class FlatpageTemplateTagTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + if flatpage_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (flatpage_middleware_class,) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + self.me = User.objects.create_user('testuser', 'test@example.com', 's3krit') + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + + def test_get_flatpages_tag(self): + "The flatpage template tag retrives unregistered prefixed flatpages by default" + out = Template( + "{% load flatpages %}" + "{% get_flatpages as flatpages %}" + "{% for page in flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context()) + self.assertEquals(out, "A Flatpage,A Nested Flatpage,") + + def test_get_flatpages_tag_for_anon_user(self): + "The flatpage template tag retrives unregistered flatpages for an anonymous user" + out = Template( + "{% load flatpages %}" + "{% get_flatpages for anonuser as flatpages %}" + "{% for page in flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context({ + 'anonuser': AnonymousUser() + })) + self.assertEquals(out, "A Flatpage,A Nested Flatpage,") + + def test_get_flatpages_tag_for_user(self): + "The flatpage template tag retrives all flatpages for an authenticated user" + out = Template( + "{% load flatpages %}" + "{% get_flatpages for me as flatpages %}" + "{% for page in flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context({ + 'me': self.me + })) + self.assertEquals(out, "A Flatpage,A Nested Flatpage,Sekrit Nested Flatpage,Sekrit Flatpage,") + + def test_get_flatpages_with_prefix(self): + "The flatpage template tag retrives unregistered prefixed flatpages by default" + out = Template( + "{% load flatpages %}" + "{% get_flatpages '/location/' as location_flatpages %}" + "{% for page in location_flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context()) + self.assertEquals(out, "A Nested Flatpage,") + + def test_get_flatpages_with_prefix_for_anon_user(self): + "The flatpage template tag retrives unregistered prefixed flatpages for an anonymous user" + out = Template( + "{% load flatpages %}" + "{% get_flatpages '/location/' for anonuser as location_flatpages %}" + "{% for page in location_flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context({ + 'anonuser': AnonymousUser() + })) + self.assertEquals(out, "A Nested Flatpage,") + + def test_get_flatpages_with_prefix_for_user(self): + "The flatpage template tag retrive prefixed flatpages for an authenticated user" + out = Template( + "{% load flatpages %}" + "{% get_flatpages '/location/' for me as location_flatpages %}" + "{% for page in location_flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context({ + 'me': self.me + })) + self.assertEquals(out, "A Nested Flatpage,Sekrit Nested Flatpage,") + + def test_get_flatpages_with_variable_prefix(self): + "The prefix for the flatpage template tag can be a template variable" + out = Template( + "{% load flatpages %}" + "{% get_flatpages location_prefix as location_flatpages %}" + "{% for page in location_flatpages %}" + "{{ page.title }}," + "{% endfor %}" + ).render(Context({ + 'location_prefix': '/location/' + })) + self.assertEquals(out, "A Nested Flatpage,") + + def test_parsing_errors(self): + "There are various ways that the flatpages template tag won't parse" + render = lambda t: Template(t).render(Context()) + + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages %}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages as %}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages cheesecake flatpages %}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages as flatpages asdf%}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages cheesecake user as flatpages %}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages for user as flatpages asdf%}") + self.assertRaises(TemplateSyntaxError, render, + "{% load flatpages %}{% get_flatpages prefix for user as flatpages asdf%}") + diff --git a/django/contrib/flatpages/tests/urls.py b/django/contrib/flatpages/tests/urls.py new file mode 100644 index 0000000000..3cffd09d0f --- /dev/null +++ b/django/contrib/flatpages/tests/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +# special urls for flatpage test cases +urlpatterns = patterns('', + (r'^flatpage_root', include('django.contrib.flatpages.urls')), + (r'^accounts/', include('django.contrib.auth.urls')), +) + diff --git a/django/contrib/flatpages/tests/views.py b/django/contrib/flatpages/tests/views.py new file mode 100644 index 0000000000..89bdde2d92 --- /dev/null +++ b/django/contrib/flatpages/tests/views.py @@ -0,0 +1,72 @@ +import os +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.flatpages.models import FlatPage +from django.test import TestCase + +class FlatpageViewTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + if flatpage_middleware_class in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES = tuple(m for m in settings.MIDDLEWARE_CLASSES if m != flatpage_middleware_class) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + + def test_view_flatpage(self): + "A flatpage can be served through a view" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it flat!</p>") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + User.objects.create_user('testuser', 'test@example.com', 's3krit') + self.client.login(username='testuser',password='s3krit') + response = self.client.get('/flatpage_root/sekrit/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it sekrit!</p>") + + def test_fallback_flatpage(self): + "A fallback flatpage won't be served if the middleware is disabled" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 404) + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage won't be served if the fallback middlware is disabled" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_flatpage_special_chars(self): + "A flatpage with special chars in the URL can be served through a view" + fp = FlatPage.objects.create( + url="/some.very_special~chars-here/", + title="A very special page", + content="Isn't it special!", + enable_comments=False, + registration_required=False, + ) + fp.sites.add(1) + + response = self.client.get('/flatpage_root/some.very_special~chars-here/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "<p>Isn't it special!</p>") diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 336600328d..88ef4da65e 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -13,10 +13,13 @@ DEFAULT_TEMPLATE = 'flatpages/default.html' # when a 404 is raised, which often means CsrfViewMiddleware.process_view # has not been called even if CsrfViewMiddleware is installed. So we need # to use @csrf_protect, in case the template needs {% csrf_token %}. -@csrf_protect +# However, we can't just wrap this view; if no matching flatpage exists, +# or a redirect is required for authentication, the 404 needs to be returned +# without any CSRF checks. Therefore, we only +# CSRF protect the internal implementation. def flatpage(request, url): """ - Flat page view. + Public interface to the flat page view. Models: `flatpages.flatpages` Templates: Uses the template defined by the ``template_name`` field, @@ -30,6 +33,13 @@ def flatpage(request, url): if not url.startswith('/'): url = "/" + url f = get_object_or_404(FlatPage, url__exact=url, sites__id__exact=settings.SITE_ID) + return render_flatpage(request, f) + +@csrf_protect +def render_flatpage(request, f): + """ + Internal interface to the flat page view. + """ # If registration is required for accessing this page, and the user isn't # logged in, redirect to the login page. if f.registration_required and not request.user.is_authenticated(): diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index 02d8fd71d4..32e27df574 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -27,7 +27,7 @@ class FormWizard(object): def __init__(self, form_list, initial=None): """ Start a new wizard with a list of forms. - + form_list should be a list of Form classes (not instances). """ self.form_list = form_list[:] @@ -37,7 +37,7 @@ class FormWizard(object): self.extra_context = {} # A zero-based counter keeping track of which step we're in. - self.step = 0 + self.step = 0 def __repr__(self): return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) @@ -48,7 +48,7 @@ class FormWizard(object): def num_steps(self): "Helper method that returns the number of steps." - # You might think we should just set "self.form_list = len(form_list)" + # You might think we should just set "self.num_steps = len(form_list)" # in __init__(), but this calculation needs to be dynamic, because some # hook methods might alter self.form_list. return len(self.form_list) diff --git a/django/contrib/gis/db/backends/mysql/creation.py b/django/contrib/gis/db/backends/mysql/creation.py index 93fd2e6166..dda77ea6ab 100644 --- a/django/contrib/gis/db/backends/mysql/creation.py +++ b/django/contrib/gis/db/backends/mysql/creation.py @@ -6,7 +6,7 @@ class MySQLCreation(DatabaseCreation): from django.contrib.gis.db.models.fields import GeometryField output = super(MySQLCreation, self).sql_indexes_for_field(model, f, style) - if isinstance(f, GeometryField): + if isinstance(f, GeometryField) and f.spatial_index: qn = self.connection.ops.quote_name db_table = model._meta.db_table idx_name = '%s_%s_id' % (db_table, f.column) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index b1a9e31529..5affcf9a18 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -233,8 +233,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): }) self.geography_operators = { 'bboverlaps' : PostGISOperator('&&'), - 'exact' : PostGISOperator('~='), - 'same_as' : PostGISOperator('~='), } # Creating a dictionary lookup of all GIS terms for PostGIS. diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index d419dab5e1..729fc152e7 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -51,7 +51,7 @@ class DatabaseWrapper(SqliteDatabaseWrapper): self.connection.create_function("django_extract", 2, _sqlite_extract) self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) self.connection.create_function("regexp", 2, _sqlite_regexp) - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) ## From here on, customized for GeoDjango ## diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 78eeeafe19..55dc4a66ec 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -95,7 +95,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): return result def get_default_columns(self, with_aliases=False, col_aliases=None, - start_alias=None, opts=None, as_pairs=False): + start_alias=None, opts=None, as_pairs=False, local_only=False): """ Computes the default columns for selecting every field in the base model. Will sometimes be called to pull in related models (e.g. via @@ -121,6 +121,8 @@ class GeoSQLCompiler(compiler.SQLCompiler): if start_alias: seen = {None: start_alias} for field, model in opts.get_fields_with_model(): + if local_only and model is not None: + continue if start_alias: try: alias = seen[model] diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 6589c5647c..a7a5658c49 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -14,10 +14,10 @@ if lib_path: lib_names = None elif os.name == 'nt': # Windows NT shared library - lib_names = ['gdal16', 'gdal15'] + lib_names = ['gdal17', 'gdal16', 'gdal15'] elif os.name == 'posix': # *NIX library names. - lib_names = ['gdal', 'GDAL', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0'] + lib_names = ['gdal', 'GDAL', 'gdal1.7.0', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0'] else: raise OGRException('Unsupported OS "%s"' % os.name) diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 0d862190c0..44d62c2a98 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,115 +1,108 @@ import sys +import unittest + +from django.conf import settings +from django.db.models import get_app +from django.test.simple import build_suite, DjangoTestSuiteRunner def run_tests(*args, **kwargs): from django.test.simple import run_tests as base_run_tests return base_run_tests(*args, **kwargs) -def geo_suite(): - """ - Builds a test suite for the GIS package. This is not named - `suite` so it will not interfere with the Django test suite (since - spatial database tables are required to execute these tests on - some backends). - """ - from django.conf import settings - from django.contrib.gis.geos import GEOS_PREPARE - from django.contrib.gis.gdal import HAS_GDAL - from django.contrib.gis.utils import HAS_GEOIP - from django.contrib.gis.tests.utils import postgis, mysql - from django.db import connection - from django.utils.importlib import import_module - - gis_tests = [] - - # Adding the GEOS tests. - from django.contrib.gis.geos import tests as geos_tests - gis_tests.append(geos_tests.suite()) - - # Tests that require use of a spatial database (e.g., creation of models) - test_apps = ['geoapp', 'relatedapp'] - if postgis and connection.ops.geography: - # Test geography support with PostGIS 1.5+. - test_apps.append('geogapp') - - # Tests that do not require setting up and tearing down a spatial database. - test_suite_names = [ - 'test_measure', - ] - - if HAS_GDAL: - # These tests require GDAL. - if not mysql: - test_apps.append('distapp') - - # Only PostGIS using GEOS 3.1+ can support 3D so far. - if postgis and GEOS_PREPARE: - test_apps.append('geo3d') - - test_suite_names.extend(['test_spatialrefsys', 'test_geoforms']) - test_apps.append('layermap') +def run_gis_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): + import warnings + warnings.warn( + 'The run_gis_tests() test runner has been deprecated in favor of GeoDjangoTestSuiteRunner.', + PendingDeprecationWarning + ) + test_runner = GeoDjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) + return test_runner.run_tests(test_labels, extra_tests=extra_tests) + +class GeoDjangoTestSuiteRunner(DjangoTestSuiteRunner): + + def setup_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).setup_test_environment(**kwargs) - # Adding the GDAL tests. - from django.contrib.gis.gdal import tests as gdal_tests - gis_tests.append(gdal_tests.suite()) - else: - print >>sys.stderr, "GDAL not available - no tests requiring GDAL will be run." - - if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): - test_suite_names.append('test_geoip') - - # Adding the rest of the suites from the modules specified - # in the `test_suite_names`. - for suite_name in test_suite_names: - tsuite = import_module('django.contrib.gis.tests.' + suite_name) - gis_tests.append(tsuite.suite()) - - return gis_tests, test_apps - -def run_gis_tests(test_labels, **kwargs): - """ - Use this routine as the TEST_RUNNER in your settings in order to run the - GeoDjango test suite. This must be done as a database superuser for - PostGIS, so read the docstring in `run_test()` below for more details. - """ - from django.conf import settings - from django.db.models import loading - from django.contrib.gis.tests.utils import mysql - - # Getting initial values. - old_installed = settings.INSTALLED_APPS - old_root_urlconf = settings.ROOT_URLCONF - - # Overridding the INSTALLED_APPS with only what we need, - # to prevent unnecessary database table creation. - new_installed = ['django.contrib.sites', - 'django.contrib.sitemaps', - 'django.contrib.gis', - ] - - # Setting the URLs. - settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls' - - # Creating the test suite, adding the test models to INSTALLED_APPS - # so they will be tested. - gis_tests, test_apps = geo_suite() - for test_model in test_apps: - module_name = 'django.contrib.gis.tests.%s' % test_model - new_installed.append(module_name) - - # Resetting the loaded flag to take into account what we appended to - # the INSTALLED_APPS (since this routine is invoked through - # django/core/management, it caches the apps; this ensures that syncdb - # will see our appended models) - settings.INSTALLED_APPS = new_installed - loading.cache.loaded = False - - kwargs['extra_tests'] = gis_tests - - # Running the tests using the GIS test runner. - result = run_tests(test_labels, **kwargs) - - # Restoring modified settings. - settings.INSTALLED_APPS = old_installed - settings.ROOT_URLCONF = old_root_urlconf - - return result + from django.db import connection + from django.contrib.gis.geos import GEOS_PREPARE + from django.contrib.gis.gdal import HAS_GDAL + + # Getting and storing the original values of INSTALLED_APPS and + # the ROOT_URLCONF. + self.old_installed = settings.INSTALLED_APPS + self.old_root_urlconf = settings.ROOT_URLCONF + + # Tests that require use of a spatial database (e.g., creation of models) + self.geo_apps = ['geoapp', 'relatedapp'] + if connection.ops.postgis and connection.ops.geography: + # Test geography support with PostGIS 1.5+. + self.geo_apps.append('geogapp') + + if HAS_GDAL: + # The following GeoDjango test apps depend on GDAL support. + if not connection.ops.mysql: + self.geo_apps.append('distapp') + + # 3D apps use LayerMapping, which uses GDAL. + if connection.ops.postgis and GEOS_PREPARE: + self.geo_apps.append('geo3d') + + self.geo_apps.append('layermap') + + # Constructing the new INSTALLED_APPS, and including applications + # within the GeoDjango test namespace (`self.geo_apps`). + new_installed = ['django.contrib.sites', + 'django.contrib.sitemaps', + 'django.contrib.gis', + ] + new_installed.extend(['django.contrib.gis.tests.%s' % app + for app in self.geo_apps]) + settings.INSTALLED_APPS = new_installed + + # Setting the URLs. + settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls' + + def teardown_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).teardown_test_environment(**kwargs) + settings.INSTALLED_APPS = self.old_installed + settings.ROOT_URLCONF = self.old_root_urlconf + + def build_suite(self, test_labels, extra_tests=None, **kwargs): + """ + This method is overridden to construct a suite consisting only of tests + for GeoDjango. + """ + suite = unittest.TestSuite() + + # Adding the GEOS tests. + from django.contrib.gis.geos import tests as geos_tests + suite.addTest(geos_tests.suite()) + + # Adding the measurment tests. + from django.contrib.gis.tests import test_measure + suite.addTest(test_measure.suite()) + + # Adding GDAL tests, and any test suite that depends on GDAL, to the + # suite if GDAL is available. + from django.contrib.gis.gdal import HAS_GDAL + if HAS_GDAL: + from django.contrib.gis.gdal import tests as gdal_tests + suite.addTest(gdal_tests.suite()) + + from django.contrib.gis.tests import test_spatialrefsys, test_geoforms + suite.addTest(test_spatialrefsys.suite()) + suite.addTest(test_geoforms.suite()) + else: + sys.stderr.write('GDAL not available - no tests requiring GDAL will be run.\n') + + # Add GeoIP tests to the suite, if the library and data is available. + from django.contrib.gis.utils import HAS_GEOIP + if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): + from django.contrib.gis.tests import test_geoip + suite.addTest(test_geoip.suite()) + + # Finally, adding the suites for each of the GeoDjango test apps. + for app_name in self.geo_apps: + suite.addTest(build_suite(get_app(app_name))) + + return suite diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py index 7be5193103..3dea930afd 100644 --- a/django/contrib/gis/tests/geogapp/tests.py +++ b/django/contrib/gis/tests/geogapp/tests.py @@ -44,6 +44,10 @@ class GeographyTest(TestCase): # `@` operator not available. self.assertRaises(ValueError, City.objects.filter(point__contained=z.poly).count) + # Regression test for #14060, `~=` was never really implemented for PostGIS. + htown = City.objects.get(name='Houston') + self.assertRaises(ValueError, City.objects.get, point__exact=htown.point) + def test05_geography_layermapping(self): "Testing LayerMapping support on models with geography fields." # There is a similar test in `layermap` that uses the same data set, diff --git a/django/contrib/gis/tests/relatedapp/models.py b/django/contrib/gis/tests/relatedapp/models.py index 726f9826c0..2e9a62b61f 100644 --- a/django/contrib/gis/tests/relatedapp/models.py +++ b/django/contrib/gis/tests/relatedapp/models.py @@ -38,6 +38,11 @@ class Author(models.Model): name = models.CharField(max_length=100) objects = models.GeoManager() +class Article(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, unique=True) + objects = models.GeoManager() + class Book(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey(Author, related_name='books', null=True) diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 184b65b9c7..5d3d00f08d 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -4,7 +4,7 @@ from django.contrib.gis.db.models import Collect, Count, Extent, F, Union from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite, no_mysql, no_oracle, no_spatialite from django.conf import settings -from models import City, Location, DirectoryEntry, Parcel, Book, Author +from models import City, Location, DirectoryEntry, Parcel, Book, Author, Article cities = (('Aurora', 'TX', -97.516111, 33.058333), ('Roswell', 'NM', -104.528056, 33.387222), @@ -291,6 +291,14 @@ class RelatedGeoModelTest(unittest.TestCase): self.assertEqual(4, len(coll)) self.assertEqual(ref_geom, coll) + def test15_invalid_select_related(self): + "Testing doing select_related on the related name manager of a unique FK. See #13934." + qs = Article.objects.select_related('author__article') + # This triggers TypeError when `get_default_columns` has no `local_only` + # keyword. The TypeError is swallowed if QuerySet is actually + # evaluated as list generation swallows TypeError in CPython. + sql = str(qs.query) + # TODO: Related tests for KML, GML, and distance lookups. def suite(): diff --git a/django/contrib/localflavor/in_/in_states.py b/django/contrib/localflavor/in_/in_states.py index 498efe7069..bb4a7482ca 100644 --- a/django/contrib/localflavor/in_/in_states.py +++ b/django/contrib/localflavor/in_/in_states.py @@ -7,43 +7,43 @@ when explicitly needed. """ STATE_CHOICES = ( - 'KA', 'Karnataka', - 'AP', 'Andhra Pradesh', - 'KL', 'Kerala', - 'TN', 'Tamil Nadu', - 'MH', 'Maharashtra', - 'UP', 'Uttar Pradesh', - 'GA', 'Goa', - 'GJ', 'Gujarat', - 'RJ', 'Rajasthan', - 'HP', 'Himachal Pradesh', - 'JK', 'Jammu and Kashmir', - 'AR', 'Arunachal Pradesh', - 'AS', 'Assam', - 'BR', 'Bihar', - 'CG', 'Chattisgarh', - 'HR', 'Haryana', - 'JH', 'Jharkhand', - 'MP', 'Madhya Pradesh', - 'MN', 'Manipur', - 'ML', 'Meghalaya', - 'MZ', 'Mizoram', - 'NL', 'Nagaland', - 'OR', 'Orissa', - 'PB', 'Punjab', - 'SK', 'Sikkim', - 'TR', 'Tripura', - 'UA', 'Uttarakhand', - 'WB', 'West Bengal', + ('KA', 'Karnataka'), + ('AP', 'Andhra Pradesh'), + ('KL', 'Kerala'), + ('TN', 'Tamil Nadu'), + ('MH', 'Maharashtra'), + ('UP', 'Uttar Pradesh'), + ('GA', 'Goa'), + ('GJ', 'Gujarat'), + ('RJ', 'Rajasthan'), + ('HP', 'Himachal Pradesh'), + ('JK', 'Jammu and Kashmir'), + ('AR', 'Arunachal Pradesh'), + ('AS', 'Assam'), + ('BR', 'Bihar'), + ('CG', 'Chattisgarh'), + ('HR', 'Haryana'), + ('JH', 'Jharkhand'), + ('MP', 'Madhya Pradesh'), + ('MN', 'Manipur'), + ('ML', 'Meghalaya'), + ('MZ', 'Mizoram'), + ('NL', 'Nagaland'), + ('OR', 'Orissa'), + ('PB', 'Punjab'), + ('SK', 'Sikkim'), + ('TR', 'Tripura'), + ('UA', 'Uttarakhand'), + ('WB', 'West Bengal'), # Union Territories - 'AN', 'Andaman and Nicobar', - 'CH', 'Chandigarh', - 'DN', 'Dadra and Nagar Haveli', - 'DD', 'Daman and Diu', - 'DL', 'Delhi', - 'LD', 'Lakshadweep', - 'PY', 'Pondicherry', + ('AN', 'Andaman and Nicobar'), + ('CH', 'Chandigarh'), + ('DN', 'Dadra and Nagar Haveli'), + ('DD', 'Daman and Diu'), + ('DL', 'Delhi'), + ('LD', 'Lakshadweep'), + ('PY', 'Pondicherry'), ) STATES_NORMALIZED = { diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py index 9a96f8cda4..6a22e530ba 100644 --- a/django/contrib/markup/tests.py +++ b/django/contrib/markup/tests.py @@ -22,7 +22,7 @@ Paragraph 2 with "quotes" and @code@""" t = Template("{{ textile_content|textile }}") rendered = t.render(Context(locals())).strip() if textile: - self.assertEqual(rendered, """<p>Paragraph 1</p> + self.assertEqual(rendered.replace('\t', ''), """<p>Paragraph 1</p> <p>Paragraph 2 with “quotes” and <code>code</code></p>""") else: diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 3f6350345f..843edca9cf 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -58,7 +58,7 @@ class SessionStore(SessionBase): finally: session_file.close() except IOError: - pass + self.create() return session_data def create(self): diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index f0a3c4ec8c..e645b73817 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,388 +1,273 @@ -r""" - ->>> from django.conf import settings ->>> from django.contrib.sessions.backends.db import SessionStore as DatabaseSession ->>> from django.contrib.sessions.backends.cache import SessionStore as CacheSession ->>> from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession ->>> from django.contrib.sessions.backends.file import SessionStore as FileSession ->>> from django.contrib.sessions.backends.base import SessionBase ->>> from django.contrib.sessions.models import Session - ->>> db_session = DatabaseSession() ->>> db_session.modified -False ->>> db_session.get('cat') ->>> db_session['cat'] = "dog" ->>> db_session.modified -True ->>> db_session.pop('cat') -'dog' ->>> db_session.pop('some key', 'does not exist') -'does not exist' ->>> db_session.save() ->>> db_session.exists(db_session.session_key) -True ->>> db_session.delete(db_session.session_key) ->>> db_session.exists(db_session.session_key) -False - ->>> db_session['foo'] = 'bar' ->>> db_session.save() ->>> db_session.exists(db_session.session_key) -True ->>> prev_key = db_session.session_key ->>> db_session.flush() ->>> db_session.exists(prev_key) -False ->>> db_session.session_key == prev_key -False ->>> db_session.modified, db_session.accessed -(True, True) ->>> db_session['a'], db_session['b'] = 'c', 'd' ->>> db_session.save() ->>> prev_key = db_session.session_key ->>> prev_data = db_session.items() ->>> db_session.cycle_key() ->>> db_session.session_key == prev_key -False ->>> db_session.items() == prev_data -True - -# Submitting an invalid session key (either by guessing, or if the db has -# removed the key) results in a new key being generated. ->>> Session.objects.filter(pk=db_session.session_key).delete() ->>> db_session = DatabaseSession(db_session.session_key) ->>> db_session.save() ->>> DatabaseSession('1').get('cat') - -# -# Cached DB session tests -# - ->>> cdb_session = CacheDBSession() ->>> cdb_session.modified -False ->>> cdb_session['cat'] = "dog" ->>> cdb_session.modified -True ->>> cdb_session.pop('cat') -'dog' ->>> cdb_session.pop('some key', 'does not exist') -'does not exist' ->>> cdb_session.save() ->>> cdb_session.exists(cdb_session.session_key) -True ->>> cdb_session.delete(cdb_session.session_key) ->>> cdb_session.exists(cdb_session.session_key) -False - -# -# File session tests. -# - -# Do file session tests in an isolated directory, and kill it after we're done. ->>> original_session_file_path = settings.SESSION_FILE_PATH ->>> import tempfile ->>> temp_session_store = settings.SESSION_FILE_PATH = tempfile.mkdtemp() - ->>> file_session = FileSession() ->>> file_session.modified -False ->>> file_session['cat'] = "dog" ->>> file_session.modified -True ->>> file_session.pop('cat') -'dog' ->>> file_session.pop('some key', 'does not exist') -'does not exist' ->>> file_session.save() ->>> file_session.exists(file_session.session_key) -True ->>> file_session.delete(file_session.session_key) ->>> file_session.exists(file_session.session_key) -False ->>> FileSession('1').get('cat') - ->>> file_session['foo'] = 'bar' ->>> file_session.save() ->>> file_session.exists(file_session.session_key) -True ->>> prev_key = file_session.session_key ->>> file_session.flush() ->>> file_session.exists(prev_key) -False ->>> file_session.session_key == prev_key -False ->>> file_session.modified, file_session.accessed -(True, True) ->>> file_session['a'], file_session['b'] = 'c', 'd' ->>> file_session.save() ->>> prev_key = file_session.session_key ->>> prev_data = file_session.items() ->>> file_session.cycle_key() ->>> file_session.session_key == prev_key -False ->>> file_session.items() == prev_data -True - ->>> Session.objects.filter(pk=file_session.session_key).delete() ->>> file_session = FileSession(file_session.session_key) ->>> file_session.save() - -# Make sure the file backend checks for a good storage dir ->>> settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer" ->>> FileSession() -Traceback (innermost last): - ... -ImproperlyConfigured: The session storage path '/if/this/directory/exists/you/have/a/weird/computer' doesn't exist. Please set your SESSION_FILE_PATH setting to an existing directory in which Django can store session data. - -# Clean up after the file tests ->>> settings.SESSION_FILE_PATH = original_session_file_path ->>> import shutil ->>> shutil.rmtree(temp_session_store) - -# -# Cache-based tests -# NB: be careful to delete any sessions created; stale sessions fill up the -# /tmp and eventually overwhelm it after lots of runs (think buildbots) -# - ->>> cache_session = CacheSession() ->>> cache_session.modified -False ->>> cache_session['cat'] = "dog" ->>> cache_session.modified -True ->>> cache_session.pop('cat') -'dog' ->>> cache_session.pop('some key', 'does not exist') -'does not exist' ->>> cache_session.save() ->>> cache_session.delete(cache_session.session_key) ->>> cache_session.exists(cache_session.session_key) -False ->>> cache_session['foo'] = 'bar' ->>> cache_session.save() ->>> cache_session.exists(cache_session.session_key) -True ->>> prev_key = cache_session.session_key ->>> cache_session.flush() ->>> cache_session.exists(prev_key) -False ->>> cache_session.session_key == prev_key -False ->>> cache_session.modified, cache_session.accessed -(True, True) ->>> cache_session['a'], cache_session['b'] = 'c', 'd' ->>> cache_session.save() ->>> prev_key = cache_session.session_key ->>> prev_data = cache_session.items() ->>> cache_session.cycle_key() ->>> cache_session.session_key == prev_key -False ->>> cache_session.items() == prev_data -True ->>> cache_session = CacheSession() ->>> cache_session.save() ->>> key = cache_session.session_key ->>> cache_session.exists(key) -True - ->>> Session.objects.filter(pk=cache_session.session_key).delete() ->>> cache_session = CacheSession(cache_session.session_key) ->>> cache_session.save() ->>> cache_session.delete(cache_session.session_key) - ->>> s = SessionBase() ->>> s._session['some key'] = 'exists' # Pre-populate the session with some data ->>> s.accessed = False # Reset to pretend this wasn't accessed previously - ->>> s.accessed, s.modified -(False, False) - ->>> s.pop('non existant key', 'does not exist') -'does not exist' ->>> s.accessed, s.modified -(True, False) - ->>> s.setdefault('foo', 'bar') -'bar' ->>> s.setdefault('foo', 'baz') -'bar' - ->>> s.accessed = False # Reset the accessed flag - ->>> s.pop('some key') -'exists' ->>> s.accessed, s.modified -(True, True) - ->>> s.pop('some key', 'does not exist') -'does not exist' - - ->>> s.get('update key', None) - -# test .update() ->>> s.modified = s.accessed = False # Reset to pretend this wasn't accessed previously ->>> s.update({'update key':1}) ->>> s.accessed, s.modified -(True, True) ->>> s.get('update key', None) -1 - -# test .has_key() ->>> s.modified = s.accessed = False # Reset to pretend this wasn't accessed previously ->>> s.has_key('update key') -True ->>> s.accessed, s.modified -(True, False) - -# test .values() ->>> s = SessionBase() ->>> s.values() -[] ->>> s.accessed -True ->>> s['x'] = 1 ->>> s.values() -[1] - -# test .iterkeys() ->>> s.accessed = False ->>> i = s.iterkeys() ->>> hasattr(i,'__iter__') -True ->>> s.accessed -True ->>> list(i) -['x'] - -# test .itervalues() ->>> s.accessed = False ->>> i = s.itervalues() ->>> hasattr(i,'__iter__') -True ->>> s.accessed -True ->>> list(i) -[1] - -# test .iteritems() ->>> s.accessed = False ->>> i = s.iteritems() ->>> hasattr(i,'__iter__') -True ->>> s.accessed -True ->>> list(i) -[('x', 1)] - -# test .clear() ->>> s.modified = s.accessed = False ->>> s.items() -[('x', 1)] ->>> s.clear() ->>> s.items() -[] ->>> s.accessed, s.modified -(True, True) - -######################### -# Custom session expiry # -######################### - ->>> from django.conf import settings ->>> from datetime import datetime, timedelta - ->>> td10 = timedelta(seconds=10) - -# A normal session has a max age equal to settings ->>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE -True - -# So does a custom session with an idle expiration time of 0 (but it'll expire -# at browser close) ->>> s.set_expiry(0) ->>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE -True - -# Custom session idle expiration time ->>> s.set_expiry(10) ->>> delta = s.get_expiry_date() - datetime.now() ->>> delta.seconds in (9, 10) -True ->>> age = s.get_expiry_age() ->>> age in (9, 10) -True - -# Custom session fixed expiry date (timedelta) ->>> s.set_expiry(td10) ->>> delta = s.get_expiry_date() - datetime.now() ->>> delta.seconds in (9, 10) -True ->>> age = s.get_expiry_age() ->>> age in (9, 10) -True - -# Custom session fixed expiry date (fixed datetime) ->>> s.set_expiry(datetime.now() + td10) ->>> delta = s.get_expiry_date() - datetime.now() ->>> delta.seconds in (9, 10) -True ->>> age = s.get_expiry_age() ->>> age in (9, 10) -True - -# Set back to default session age ->>> s.set_expiry(None) ->>> s.get_expiry_age() == settings.SESSION_COOKIE_AGE -True - -# Allow to set back to default session age even if no alternate has been set ->>> s.set_expiry(None) - - -# We're changing the setting then reverting back to the original setting at the -# end of these tests. ->>> original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE ->>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False - -# Custom session age ->>> s.set_expiry(10) ->>> s.get_expire_at_browser_close() -False - -# Custom expire-at-browser-close ->>> s.set_expiry(0) ->>> s.get_expire_at_browser_close() -True - -# Default session age ->>> s.set_expiry(None) ->>> s.get_expire_at_browser_close() -False - ->>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True - -# Custom session age ->>> s.set_expiry(10) ->>> s.get_expire_at_browser_close() -False - -# Custom expire-at-browser-close ->>> s.set_expiry(0) ->>> s.get_expire_at_browser_close() -True - -# Default session age ->>> s.set_expiry(None) ->>> s.get_expire_at_browser_close() -True - ->>> settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close -""" - -if __name__ == '__main__': - import doctest - doctest.testmod() +from datetime import datetime, timedelta +from django.conf import settings +from django.contrib.sessions.backends.db import SessionStore as DatabaseSession +from django.contrib.sessions.backends.cache import SessionStore as CacheSession +from django.contrib.sessions.backends.cached_db import SessionStore as CacheDBSession +from django.contrib.sessions.backends.file import SessionStore as FileSession +from django.contrib.sessions.backends.base import SessionBase +from django.contrib.sessions.models import Session +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +import shutil +import tempfile +import unittest + + +class SessionTestsMixin(object): + # This does not inherit from TestCase to avoid any tests being run with this + # class, which wouldn't work, and to allow different TestCase subclasses to + # be used. + + backend = None # subclasses must specify + + def setUp(self): + self.session = self.backend() + + def tearDown(self): + # NB: be careful to delete any sessions created; stale sessions fill up + # the /tmp (with some backends) and eventually overwhelm it after lots + # of runs (think buildbots) + self.session.delete() + + def test_new_session(self): + self.assertFalse(self.session.modified) + self.assertFalse(self.session.accessed) + + def test_get_empty(self): + self.assertEqual(self.session.get('cat'), None) + + def test_store(self): + self.session['cat'] = "dog" + self.assertTrue(self.session.modified) + self.assertEqual(self.session.pop('cat'), 'dog') + + def test_pop(self): + self.session['some key'] = 'exists' + # Need to reset these to pretend we haven't accessed it: + self.accessed = False + self.modified = False + + self.assertEqual(self.session.pop('some key'), 'exists') + self.assertTrue(self.session.accessed) + self.assertTrue(self.session.modified) + self.assertEqual(self.session.get('some key'), None) + + def test_pop_default(self): + self.assertEqual(self.session.pop('some key', 'does not exist'), + 'does not exist') + self.assertTrue(self.session.accessed) + self.assertFalse(self.session.modified) + + def test_setdefault(self): + self.assertEqual(self.session.setdefault('foo', 'bar'), 'bar') + self.assertEqual(self.session.setdefault('foo', 'baz'), 'bar') + self.assertTrue(self.session.accessed) + self.assertTrue(self.session.modified) + + def test_update(self): + self.session.update({'update key': 1}) + self.assertTrue(self.session.accessed) + self.assertTrue(self.session.modified) + self.assertEqual(self.session.get('update key', None), 1) + + def test_has_key(self): + self.session['some key'] = 1 + self.session.modified = False + self.session.accessed = False + self.assertTrue(self.session.has_key('some key')) + self.assertTrue(self.session.accessed) + self.assertFalse(self.session.modified) + + def test_values(self): + self.assertEqual(self.session.values(), []) + self.assertTrue(self.session.accessed) + self.session['some key'] = 1 + self.assertEqual(self.session.values(), [1]) + + def test_iterkeys(self): + self.session['x'] = 1 + self.session.modified = False + self.session.accessed = False + i = self.session.iterkeys() + self.assertTrue(hasattr(i, '__iter__')) + self.assertTrue(self.session.accessed) + self.assertFalse(self.session.modified) + self.assertEqual(list(i), ['x']) + + def test_iterkeys(self): + self.session['x'] = 1 + self.session.modified = False + self.session.accessed = False + i = self.session.itervalues() + self.assertTrue(hasattr(i, '__iter__')) + self.assertTrue(self.session.accessed) + self.assertFalse(self.session.modified) + self.assertEqual(list(i), [1]) + + def test_iteritems(self): + self.session['x'] = 1 + self.session.modified = False + self.session.accessed = False + i = self.session.iteritems() + self.assertTrue(hasattr(i, '__iter__')) + self.assertTrue(self.session.accessed) + self.assertFalse(self.session.modified) + self.assertEqual(list(i), [('x',1)]) + + def test_clear(self): + self.session['x'] = 1 + self.session.modified = False + self.session.accessed = False + self.assertEqual(self.session.items(), [('x',1)]) + self.session.clear() + self.assertEqual(self.session.items(), []) + self.assertTrue(self.session.accessed) + self.assertTrue(self.session.modified) + + def test_save(self): + self.session.save() + self.assertTrue(self.session.exists(self.session.session_key)) + + def test_delete(self): + self.session.delete(self.session.session_key) + self.assertFalse(self.session.exists(self.session.session_key)) + + def test_flush(self): + self.session['foo'] = 'bar' + self.session.save() + prev_key = self.session.session_key + self.session.flush() + self.assertFalse(self.session.exists(prev_key)) + self.assertNotEqual(self.session.session_key, prev_key) + self.assertTrue(self.session.modified) + self.assertTrue(self.session.accessed) + + def test_cycle(self): + self.session['a'], self.session['b'] = 'c', 'd' + self.session.save() + prev_key = self.session.session_key + prev_data = self.session.items() + self.session.cycle_key() + self.assertNotEqual(self.session.session_key, prev_key) + self.assertEqual(self.session.items(), prev_data) + + def test_invalid_key(self): + # Submitting an invalid session key (either by guessing, or if the db has + # removed the key) results in a new key being generated. + session = self.backend('1') + session.save() + self.assertNotEqual(session.session_key, '1') + self.assertEqual(session.get('cat'), None) + session.delete() + + # Custom session expiry + def test_default_expiry(self): + # A normal session has a max age equal to settings + self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE) + + # So does a custom session with an idle expiration time of 0 (but it'll + # expire at browser close) + self.session.set_expiry(0) + self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE) + + def test_custom_expiry_seconds(self): + # Using seconds + self.session.set_expiry(10) + delta = self.session.get_expiry_date() - datetime.now() + self.assertTrue(delta.seconds in (9, 10)) + + age = self.session.get_expiry_age() + self.assertTrue(age in (9, 10)) + + def test_custom_expiry_timedelta(self): + # Using timedelta + self.session.set_expiry(timedelta(seconds=10)) + delta = self.session.get_expiry_date() - datetime.now() + self.assertTrue(delta.seconds in (9, 10)) + + age = self.session.get_expiry_age() + self.assertTrue(age in (9, 10)) + + def test_custom_expiry_timedelta(self): + # Using timedelta + self.session.set_expiry(datetime.now() + timedelta(seconds=10)) + delta = self.session.get_expiry_date() - datetime.now() + self.assertTrue(delta.seconds in (9, 10)) + + age = self.session.get_expiry_age() + self.assertTrue(age in (9, 10)) + + def test_custom_expiry_reset(self): + self.session.set_expiry(None) + self.session.set_expiry(10) + self.session.set_expiry(None) + self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE) + + def test_get_expire_at_browser_close(self): + # Tests get_expire_at_browser_close with different settings and different + # set_expiry calls + try: + try: + original_expire_at_browser_close = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE + settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = False + + self.session.set_expiry(10) + self.assertFalse(self.session.get_expire_at_browser_close()) + + self.session.set_expiry(0) + self.assertTrue(self.session.get_expire_at_browser_close()) + + self.session.set_expiry(None) + self.assertFalse(self.session.get_expire_at_browser_close()) + + settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = True + + self.session.set_expiry(10) + self.assertFalse(self.session.get_expire_at_browser_close()) + + self.session.set_expiry(0) + self.assertTrue(self.session.get_expire_at_browser_close()) + + self.session.set_expiry(None) + self.assertTrue(self.session.get_expire_at_browser_close()) + + except: + raise + finally: + settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close + + +class DatabaseSessionTests(SessionTestsMixin, TestCase): + + backend = DatabaseSession + + +class CacheDBSessionTests(SessionTestsMixin, TestCase): + + backend = CacheDBSession + +# Don't need DB flushing for these tests, so can use unittest.TestCase as base class +class FileSessionTests(SessionTestsMixin, unittest.TestCase): + + backend = FileSession + + def setUp(self): + super(FileSessionTests, self).setUp() + # Do file session tests in an isolated directory, and kill it after we're done. + self.original_session_file_path = settings.SESSION_FILE_PATH + self.temp_session_store = settings.SESSION_FILE_PATH = tempfile.mkdtemp() + + def tearDown(self): + settings.SESSION_FILE_PATH = self.original_session_file_path + shutil.rmtree(self.temp_session_store) + super(FileSessionTests, self).tearDown() + + def test_configuration_check(self): + # Make sure the file backend checks for a good storage dir + settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer" + self.assertRaises(ImproperlyConfigured, self.backend) + + +class CacheSessionTests(SessionTestsMixin, unittest.TestCase): + + backend = CacheSession diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index f877317f16..eaa7f85de9 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -65,11 +65,12 @@ class Sitemap(object): urls = [] for item in self.paginator.page(page).object_list: loc = "http://%s%s" % (current_site.domain, self.__get('location', item)) + priority = self.__get('priority', item, None) url_info = { 'location': loc, 'lastmod': self.__get('lastmod', item, None), 'changefreq': self.__get('changefreq', item, None), - 'priority': self.__get('priority', item, None) + 'priority': str(priority is not None and priority or '') } urls.append(url_info) return urls @@ -78,7 +79,7 @@ class FlatPageSitemap(Sitemap): def items(self): from django.contrib.sites.models import Site current_site = Site.objects.get_current() - return current_site.flatpage_set.all() + return current_site.flatpage_set.filter(registration_required=False) class GenericSitemap(Sitemap): priority = None diff --git a/django/contrib/sitemaps/models.py b/django/contrib/sitemaps/models.py new file mode 100644 index 0000000000..7ff128fa69 --- /dev/null +++ b/django/contrib/sitemaps/models.py @@ -0,0 +1 @@ +# This file intentionally left blank
\ No newline at end of file diff --git a/django/contrib/sitemaps/tests/__init__.py b/django/contrib/sitemaps/tests/__init__.py new file mode 100644 index 0000000000..c5b483cde2 --- /dev/null +++ b/django/contrib/sitemaps/tests/__init__.py @@ -0,0 +1 @@ +from django.contrib.sitemaps.tests.basic import * diff --git a/django/contrib/sitemaps/tests/basic.py b/django/contrib/sitemaps/tests/basic.py new file mode 100644 index 0000000000..ad04db258f --- /dev/null +++ b/django/contrib/sitemaps/tests/basic.py @@ -0,0 +1,77 @@ +from datetime import date +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.flatpages.models import FlatPage +from django.test import TestCase +from django.utils.formats import localize +from django.utils.translation import activate + + +class SitemapTests(TestCase): + urls = 'django.contrib.sitemaps.tests.urls' + + def setUp(self): + self.old_USE_L10N = settings.USE_L10N + # Create a user that will double as sitemap content + User.objects.create_user('testuser', 'test@example.com', 's3krit') + + def tearDown(self): + settings.USE_L10N = self.old_USE_L10N + + def test_simple_sitemap(self): + "A simple sitemap can be rendered" + # Retrieve the sitemap. + response = self.client.get('/simple/sitemap.xml') + # Check for all the important bits: + self.assertEquals(response.content, """<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +<url><loc>http://example.com/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> +</urlset> +""" % date.today().strftime('%Y-%m-%d')) + + def test_localized_priority(self): + "The priority value should not be localized (Refs #14164)" + # Localization should be active + settings.USE_L10N = True + activate('fr') + self.assertEqual(u'0,3', localize(0.3)) + + # Retrieve the sitemap. Check that priorities + # haven't been rendered in localized format + response = self.client.get('/simple/sitemap.xml') + self.assertContains(response, '<priority>0.5</priority>') + self.assertContains(response, '<lastmod>%s</lastmod>' % date.today().strftime('%Y-%m-%d')) + + def test_generic_sitemap(self): + "A minimal generic sitemap can be rendered" + # Retrieve the sitemap. + response = self.client.get('/generic/sitemap.xml') + # Check for all the important bits: + self.assertEquals(response.content, """<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +<url><loc>http://example.com/users/testuser/</loc></url> +</urlset> +""") + + def test_flatpage_sitemap(self): + "Basic FlatPage sitemap test" + public = FlatPage.objects.create( + url=u'/public/', + title=u'Public Page', + enable_comments=True, + registration_required=False, + ) + public.sites.add(settings.SITE_ID) + private = FlatPage.objects.create( + url=u'/private/', + title=u'Public Page', + enable_comments=True, + registration_required=True + ) + private.sites.add(settings.SITE_ID) + response = self.client.get('/flatpages/sitemap.xml') + # Public flatpage should be in the sitemap + self.assertContains(response, '<loc>http://example.com%s</loc>' % public.url) + # Private flatpage should not be in the sitemap + self.assertNotContains(response, '<loc>http://example.com%s</loc>' % private.url) + diff --git a/django/contrib/sitemaps/tests/urls.py b/django/contrib/sitemaps/tests/urls.py new file mode 100644 index 0000000000..6cdba36b02 --- /dev/null +++ b/django/contrib/sitemaps/tests/urls.py @@ -0,0 +1,33 @@ +from datetime import datetime +from django.conf.urls.defaults import * +from django.contrib.sitemaps import Sitemap, GenericSitemap, FlatPageSitemap +from django.contrib.auth.models import User + +class SimpleSitemap(Sitemap): + changefreq = "never" + priority = 0.5 + location = '/location/' + lastmod = datetime.now() + + def items(self): + return [object()] + +simple_sitemaps = { + 'simple': SimpleSitemap, +} + +generic_sitemaps = { + 'generic': GenericSitemap({ + 'queryset': User.objects.all() + }), +} + +flatpage_sitemaps = { + 'flatpages': FlatPageSitemap, +} + +urlpatterns = patterns('django.contrib.sitemaps.views', + (r'^simple/sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps}), + (r'^generic/sitemap\.xml$', 'sitemap', {'sitemaps': generic_sitemaps}), + (r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}), +) |