Repository: django-cms/django-cms

Issue:
Document redirection behavior
A proper documentation for redirection when in frontend mode must be written.

Currently the behavior is the following:
- after creating or editing a new page or a model with toolbar which implements `Toolbar.watch_models` (see #3889) the frontend is redirected to the instance absolute URL
- after deleting the object, frontend is redirected to the home page (a better behavior would be to redirect the user to the parent page)
- after logout, user is redirected to the home page (ditto)



Discussion:

Resolving patch:
diff --git a/cms/admin/pageadmin.py b/cms/admin/pageadmin.py
--- a/cms/admin/pageadmin.py
+++ b/cms/admin/pageadmin.py
@@ -1357,9 +1357,11 @@ def add_page_type(self, request):
     def resolve(self, request):
         if not request.user.is_staff:
             if DJANGO_1_4:
-                return HttpResponse('', mimetype='text/plain')
+                return HttpResponse('/', mimetype='text/plain')
             else:
-                return HttpResponse('', content_type='text/plain')
+                return HttpResponse('/', content_type='text/plain')
+        obj = False
+        url = False
         if request.session.get('cms_log_latest', False):
             log = LogEntry.objects.get(pk=request.session['cms_log_latest'])
             try:
@@ -1368,21 +1370,45 @@ def resolve(self, request):
                 obj = None
             del request.session['cms_log_latest']
             if obj and obj.__class__ in toolbar_pool.get_watch_models() and hasattr(obj, 'get_absolute_url'):
+                # This is a test if the object url can be retrieved
+                # In case it can't, object it's not taken into account
                 try:
-                    return HttpResponse(force_unicode(obj.get_absolute_url()), content_type='text/plain')
+                    force_unicode(obj.get_absolute_url())
                 except:
-                    pass
-        pk = request.REQUEST.get('pk')
-        full_model = request.REQUEST.get('model')
-        if pk and full_model:
-            app_label, model = full_model.split('.')
-            if pk and app_label:
-                ctype = ContentType.objects.get(app_label=app_label, model=model)
-                try:
-                    instance = ctype.get_object_for_this_type(pk=pk)
-                except ctype.model_class().DoesNotExist:
-                    return HttpResponse('/', content_type='text/plain')
-                return HttpResponse(force_unicode(instance.get_absolute_url()), content_type='text/plain')
+                    obj = None
+            else:
+                obj = None
+        if not obj:
+            pk = request.REQUEST.get('pk')
+            full_model = request.REQUEST.get('model')
+            if pk and full_model:
+                app_label, model = full_model.split('.')
+                if pk and app_label:
+                    ctype = ContentType.objects.get(app_label=app_label, model=model)
+                    try:
+                        obj = ctype.get_object_for_this_type(pk=pk)
+                    except ctype.model_class().DoesNotExist:
+                        obj = None
+                    try:
+                        force_unicode(obj.get_absolute_url())
+                    except:
+                        obj = None
+        if obj:
+            if not request.toolbar or not request.toolbar.edit_mode:
+                if isinstance(obj, Page):
+                    if obj.get_public_object():
+                        url = obj.get_public_object().get_absolute_url()
+                    else:
+                        url = '%s?%s' % (
+                            obj.get_draft_object().get_absolute_url(),
+                            get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
+                        )
+                else:
+                    url = obj.get_absolute_url()
+            else:
+                url = obj.get_absolute_url()
+        if url:
+            return HttpResponse(force_unicode(url), content_type='text/plain')
         return HttpResponse('', content_type='text/plain')
 
     def lookup_allowed(self, key, *args, **kwargs):


Test patch:
diff --git a/cms/test_utils/project/placeholderapp/cms_toolbar.py b/cms/test_utils/project/placeholderapp/cms_toolbar.py
--- a/cms/test_utils/project/placeholderapp/cms_toolbar.py
+++ b/cms/test_utils/project/placeholderapp/cms_toolbar.py
@@ -5,12 +5,15 @@
 from cms.toolbar_pool import toolbar_pool
 from cms.utils.urlutils import admin_reverse
 from django.utils.translation import ugettext_lazy as _
+from .models import Example1, CharPksExample
 
 SAMPLEAPP_BREAK = 'Example1 App Break'
 
 
 @toolbar_pool.register
 class Example1Toolbar(CMSToolbar):
+    watch_models = [Example1, CharPksExample]
+
     def populate(self):
         admin_menu = self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
         position = admin_menu.find_first(Break, identifier=ADMINISTRATION_BREAK)
diff --git a/cms/test_utils/project/placeholderapp/migrations_django/0003_example1_publish.py b/cms/test_utils/project/placeholderapp/migrations_django/0003_example1_publish.py
new file mode 100644
--- /dev/null
+++ b/cms/test_utils/project/placeholderapp/migrations_django/0003_example1_publish.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('placeholderapp', '0002_charpksexample'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='example1',
+            name='publish',
+            field=models.BooleanField(default=True),
+            preserve_default=True,
+        ),
+    ]
diff --git a/cms/test_utils/project/placeholderapp/models.py b/cms/test_utils/project/placeholderapp/models.py
--- a/cms/test_utils/project/placeholderapp/models.py
+++ b/cms/test_utils/project/placeholderapp/models.py
@@ -23,6 +23,7 @@ class Example1(models.Model):
     char_4 = models.CharField(u'char_4', max_length=255)
     date_field = models.DateField(null=True)
     placeholder = PlaceholderField('placeholder')
+    publish = models.BooleanField(default=True)
 
     static_admin_url = ''
 
diff --git a/cms/test_utils/project/placeholderapp/views.py b/cms/test_utils/project/placeholderapp/views.py
--- a/cms/test_utils/project/placeholderapp/views.py
+++ b/cms/test_utils/project/placeholderapp/views.py
@@ -59,7 +59,10 @@ def list_view(request):
 
 def detail_view(request, pk, template_name='detail.html', item_name="char_1",
                 template_string='',):
-    instance = Example1.objects.get(pk=pk)
+    if request.user.is_staff and request.toolbar:
+        instance = Example1.objects.get(pk=pk)
+    else:
+        instance = Example1.objects.get(pk=pk, publish=True)
     return _base_detail(request, instance, template_name, item_name,
                         template_string)
 
diff --git a/cms/tests/apphooks.py b/cms/tests/apphooks.py
--- a/cms/tests/apphooks.py
+++ b/cms/tests/apphooks.py
@@ -1,8 +1,11 @@
 # -*- coding: utf-8 -*-
 from __future__ import with_statement
 import sys
-from django.contrib.auth.models import Permission
 
+from django.contrib.admin.models import CHANGE
+from django.contrib.admin.models import LogEntry
+from django.contrib.auth.models import Permission
+from django.contrib.contenttypes.models import ContentType
 from django.core.urlresolvers import clear_url_caches, reverse
 from django.utils import six
 from django.utils.timezone import now
@@ -12,7 +15,7 @@
 from cms.apphook_pool import apphook_pool
 from cms.appresolver import applications_page_check, clear_app_resolvers, get_app_patterns
 from cms.cms_toolbar import PlaceholderToolbar
-from cms.models import Title
+from cms.models import Title, Page
 from cms.test_utils.project.placeholderapp.models import Example1
 from cms.test_utils.testcases import CMSTestCase, SettingsOverrideTestCase
 from cms.test_utils.util.context_managers import SettingsOverride
@@ -21,6 +24,7 @@
 from cms.utils.compat.dj import get_user_model
 from cms.utils.conf import get_cms_setting
 from cms.utils.i18n import force_language
+from cms.utils.urlutils import admin_reverse
 from menus.utils import DefaultLanguageChanger
 
 
@@ -71,6 +75,19 @@ def reload_urls(self):
             if module in sys.modules:
                 del sys.modules[module]
 
+    def _fake_logentry(self, instance_id, user, text, model=Page):
+        LogEntry.objects.log_action(
+            user_id=user.id,
+            content_type_id=ContentType.objects.get_for_model(model).pk,
+            object_id=instance_id,
+            object_repr=text,
+            action_flag=CHANGE,
+        )
+        entry = LogEntry.objects.filter(user=user, action_flag__in=(CHANGE,))[0]
+        session = self.client.session
+        session['cms_log_latest'] = entry.pk
+        session.save()
+
     def create_base_structure(self, apphook, title_langs, namespace=None):
         apphook_pool.clear()
         superuser = get_user_model().objects.create_superuser('admin', 'admin@admin.com', 'admin')
@@ -570,7 +587,7 @@ def test_toolbar_multiple_supported_apps(self):
             self.assertTrue(toolbar.toolbars['cms.test_utils.project.placeholderapp.cms_toolbar.Example1Toolbar'].is_current_app)
 
     def test_toolbar_staff(self):
-        # Test that the toolbar contains edito mode switcher if placeholders are available
+        # Test that the toolbar contains edit mode switcher if placeholders are available
         apphooks = (
             'cms.test_utils.project.placeholderapp.cms_app.Example1App',
         )
@@ -593,7 +610,7 @@ def test_toolbar_staff(self):
             self.user = self._create_user('staff', True, False)
             with self.login_user_context(self.user):
                 response = self.client.get(path+"?edit")
-            response.context['request'].user = self.user
+            response.context['request'].user = get_user_model().objects.get(pk=self.user.pk)
             toolbar = CMSToolbar(response.context['request'])
             toolbar.populate()
             placeholder_toolbar = PlaceholderToolbar(response.context['request'], toolbar, True, path)
@@ -605,7 +622,7 @@ def test_toolbar_staff(self):
             self.user.user_permissions.add(Permission.objects.get(codename='change_example1'))
             with self.login_user_context(self.user):
                 response = self.client.get(path+"?edit")
-            response.context['request'].user = self.user
+            response.context['request'].user = get_user_model().objects.get(pk=self.user.pk)
             toolbar = CMSToolbar(response.context['request'])
             toolbar.populate()
             placeholder_toolbar = PlaceholderToolbar(response.context['request'], toolbar, True, path)
@@ -616,6 +633,21 @@ def test_toolbar_staff(self):
 
             self.user = None
 
+    def test_page_edit_redirect_models(self):
+        apphooks = (
+            'cms.test_utils.project.placeholderapp.cms_app.Example1App',
+        )
+        ex1 = Example1.objects.create(char_1="char_1", char_2="char_2",
+                                      char_3="char_3", char_4="char_4")
+        with SettingsOverride(CMS_APPHOOKS=apphooks, ROOT_URLCONF='cms.test_utils.project.placeholderapp_urls'):
+            self.create_base_structure('Example1App', 'en')
+            url = admin_reverse('cms_page_resolve')
+            self.user = self._create_user('admin_staff', True, True)
+            with self.login_user_context(self.user):
+                # parameters - non page object
+                response = self.client.post(url, {'pk': ex1.pk, 'model': 'placeholderapp.example1'})
+                self.assertEqual(response.content.decode('utf-8'), ex1.get_absolute_url())
+
 
 class ApphooksPageLanguageUrlTestCase(SettingsOverrideTestCase):
     settings_overrides = {'ROOT_URLCONF': 'cms.test_utils.project.second_urls_for_apphook_tests'}
diff --git a/cms/tests/toolbar.py b/cms/tests/toolbar.py
--- a/cms/tests/toolbar.py
+++ b/cms/tests/toolbar.py
@@ -4,13 +4,15 @@
 import re
 
 from django.contrib import admin
+from django.contrib.admin.models import LogEntry, CHANGE
 from django.contrib.auth.models import AnonymousUser, Permission
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
 from django.template.defaultfilters import truncatewords
 from django.test import TestCase
 from django.test.client import RequestFactory
 from django.utils.functional import lazy
 from django.utils.translation import ugettext_lazy as _, override
-from django.core.urlresolvers import reverse
 
 from cms.api import create_page, create_title, add_plugin
 from cms.cms_toolbar import ADMIN_MENU_IDENTIFIER, ADMINISTRATION_BREAK
@@ -69,6 +71,19 @@ def get_superuser(self):
         superuser = self._create_user('superuser', True, True)
         return superuser
 
+    def _fake_logentry(self, instance_id, user, text, model=Page):
+        LogEntry.objects.log_action(
+            user_id=user.id,
+            content_type_id=ContentType.objects.get_for_model(model).pk,
+            object_id=instance_id,
+            object_repr=text,
+            action_flag=CHANGE,
+        )
+        entry = LogEntry.objects.filter(user=user, object_id=instance_id, action_flag__in=(CHANGE,))[0]
+        session = self.client.session
+        session['cms_log_latest'] = entry.pk
+        session.save()
+
 
 class ToolbarTests(ToolbarTestBase):
     settings_overrides = {'CMS_PERMISSION': False}
@@ -341,34 +356,130 @@ def test_out_of_order(self):
 
     def test_page_create_redirect(self):
         superuser = self.get_superuser()
-        create_page("home", "nav_playground.html", "en",
+        page = create_page("home", "nav_playground.html", "en",
                            published=True)
-        resolve_url = admin_reverse('cms_page_resolve')
+        resolve_url_on = '%s?%s' % (admin_reverse('cms_page_resolve'),
+                                    get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+        resolve_url_off = '%s?%s' % (admin_reverse('cms_page_resolve'),
+                                     get_cms_setting('CMS_TOOLBAR_URL__EDIT_OFF'))
         with self.login_user_context(superuser):
-            response = self.client.post(resolve_url, {'pk': '', 'model': 'cms.page'})
+            response = self.client.post(resolve_url_on, {'pk': '', 'model': 'cms.page'})
             self.assertEqual(response.content.decode('utf-8'), '')
-            page_data = self.get_new_page_data()
+            page_data = self.get_new_page_data(parent_id=page.pk)
             self.client.post(URL_CMS_PAGE_ADD, page_data)
 
-            response = self.client.post(resolve_url, {'pk': Page.objects.all()[2].pk, 'model': 'cms.page'})
+            # test redirection when toolbar is in edit mode
+            response = self.client.post(resolve_url_on, {'pk': Page.objects.all()[2].pk,
+                                                         'model': 'cms.page'})
             self.assertEqual(response.content.decode('utf-8'), '/en/test-page-1/')
 
-    def test_page_edit_redirect(self):
+            self.client.post(URL_CMS_PAGE_ADD, page_data)
+
+            # test redirection when toolbar is not in edit mode
+            response = self.client.post(resolve_url_off, {'pk': Page.objects.all()[2].pk,
+                                                          'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '/en/')
+
+    def test_page_edit_redirect_editmode(self):
         page1 = create_page("home", "nav_playground.html", "en",
                             published=True)
         page2 = create_page("test", "nav_playground.html", "en",
                             published=True)
+        page3 = create_page("non-pub", "nav_playground.html", "en",
+                            published=False)
         superuser = self.get_superuser()
         with self.login_user_context(superuser):
             page_data = self.get_new_page_data()
             self.client.post(URL_CMS_PAGE_CHANGE % page2.pk, page_data)
-            url = admin_reverse('cms_page_resolve')
+            url = '%s?%s' % (admin_reverse('cms_page_resolve'),
+                             get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+            # first call returns the latest modified page with updated slug even if a different
+            # page has been requested
             response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
             self.assertEqual(response.content.decode('utf-8'), '/en/test-page-1/')
+            # following call returns the actual page requested
             response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
             self.assertEqual(response.content.decode('utf-8'), '/en/')
-        response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
-        self.assertEqual(response.content.decode('utf-8'), '')
+            # non published page - staff user can access it
+            response = self.client.post(url, {'pk': page3.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '/en/non-pub/')
+        # anonymous users should be redirected to the root page
+        response = self.client.post(url, {'pk': page3.pk, 'model': 'cms.page'})
+        self.assertEqual(response.content.decode('utf-8'), '/')
+
+    def test_page_edit_redirect_no_editmode(self):
+        page1 = create_page("home", "nav_playground.html", "en",
+                            published=True)
+        page2 = create_page("test", "nav_playground.html", "en",
+                            published=True, parent=page1)
+        page3 = create_page("non-pub-1", "nav_playground.html", "en",
+                            published=False, parent=page2)
+        page4 = create_page("non-pub-2", "nav_playground.html", "en",
+                            published=False, parent=page3)
+        superuser = self.get_superuser()
+        url = admin_reverse('cms_page_resolve')
+        with self.login_user_context(superuser):
+            # checking the redirect by passing URL parameters
+            # redirect to the same page
+            response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), page1.get_absolute_url())
+            # redirect to the same page
+            response = self.client.post(url, {'pk': page2.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), page2.get_absolute_url())
+            # redirect to the first published ancestor
+            response = self.client.post(url, {'pk': page3.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '%s?%s' % (
+                page3.get_absolute_url(),
+                get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+            )
+            # redirect to the first published ancestor
+            response = self.client.post(url, {'pk': page4.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '%s?%s' % (
+                page4.get_absolute_url(),
+                get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+            )
+
+            # checking the redirect by setting the session data
+            self._fake_logentry(page1.get_draft_object().pk, superuser, 'test page')
+            response = self.client.post(url, {'pk': page2.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), page1.get_public_object().get_absolute_url())
+
+            self._fake_logentry(page2.get_draft_object().pk, superuser, 'test page')
+            response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), page2.get_public_object().get_absolute_url())
+
+            self._fake_logentry(page3.get_draft_object().pk, superuser, 'test page')
+            response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '%s?%s' % (
+                page3.get_absolute_url(),
+                get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+            )
+
+            self._fake_logentry(page4.get_draft_object().pk, superuser, 'test page')
+            response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '%s?%s' % (
+                page4.get_absolute_url(),
+                get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
+            )
+
+    def test_page_edit_redirect_errors(self):
+        page1 = create_page("home", "nav_playground.html", "en",
+                            published=True)
+        page2 = create_page("test", "nav_playground.html", "en",
+                            published=True, parent=page1)
+        create_page("non-pub", "nav_playground.html", "en",
+                    published=False, parent=page2)
+        superuser = self.get_superuser()
+        url = admin_reverse('cms_page_resolve')
+
+        with self.login_user_context(superuser):
+            # logentry - non existing id - parameter is used
+            self._fake_logentry(9999, superuser, 'test page')
+            response = self.client.post(url, {'pk': page2.pk, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), page2.get_public_object().get_absolute_url())
+            # parameters - non existing id - no redirection
+            response = self.client.post(url, {'pk': 9999, 'model': 'cms.page'})
+            self.assertEqual(response.content.decode('utf-8'), '')
 
     def get_username(self, user=None, default=''):
         user = user or self.request.user
@@ -398,7 +509,7 @@ def test_toolbar_logout(self):
             superuser.save()
 
         page = create_page("home", "nav_playground.html", "en",
-                            published=True)
+                           published=True)
         page.publish('en')
         self.get_page_request(page, superuser, '/')
         #