Repository: django-cms/django-cms

Issue:
3.0 dirties the session for every staff request and adds expensive queries
I was trying out Django CMS 3.0 and noticed that when I log in as a staff user, Django always does an UPDATE to the auth_session table, even for requests that should be read-only. I tracked the change down to commit a4b012848976132f768d876675f4112f2966217f , which for some odd reason needs to store the per-user LogEntry count in the session:

``` python
if request.user.is_staff:
    request.session['cms_log_entries'] = LogEntry.objects.filter(user=request.user).count()
```

It seems to me that `cms_log_entries` is only needed for the duration of the request, so why not store it in e.g. `request._cms_log_entries` instead? There's no need to put it in the session.

I don't quite understand the reasoning behind this code, but the whole thing smells hacky to me. Aside from dirtying the session, it can also be potentially expensive for users with lots of edits -- all of them need to be counted **twice** on each request. Perhaps using signals to track new LogEntry rows is a nicer solution? Or always try to keep track of the latest id, so the database only needs to visit recent LogEntry rows.

Also, using magic values?

```
if log.action_flag == 1 or log.action_flag == 2:
```

This should be `log.is_addition() or log.is_change()` or `log.action_flag in (ADDITION, CHANGE)` using the constants declared in django.contrib.admin.models.



Discussion:
All of this code assumes that the latest LogEntry row is the one created by the current request, which isn't necessarily true in face of concurrency or if a single request might create multiple logs. Also if you just want the latest one, why bother with all the counting?

The idea behind all this hackery is that we can detect if you create a new instance of something in a sidebar or in a modal and then redirect to this. We also need to detect if you change the url of something via sidebar or modal and redirect to the new url afterwards. It is true that we actually don't need to save the log entrie numbers in the session but we could save it on the request.... i will create a PR.


Resolving patch:
diff --git a/cms/middleware/toolbar.py b/cms/middleware/toolbar.py
--- a/cms/middleware/toolbar.py
+++ b/cms/middleware/toolbar.py
@@ -4,7 +4,7 @@
 """
 from cms.toolbar.toolbar import CMSToolbar
 from cms.utils.i18n import force_language
-from django.contrib.admin.models import LogEntry
+from django.contrib.admin.models import LogEntry, ADDITION, CHANGE
 from menus.menu_pool import menu_pool
 from django.http import HttpResponse
 from django.template.loader import render_to_string
@@ -13,6 +13,7 @@
 
 def toolbar_plugin_processor(instance, placeholder, rendered_content, original_context):
     from cms.plugin_pool import plugin_pool
+
     original_context.push()
     child_plugin_classes = []
     plugin_class = instance.get_plugin_class()
@@ -66,7 +67,13 @@ def process_request(self, request):
         if 'build' in request.GET and not request.session.get('cms_build', False):
             request.session['cms_build'] = True
         if request.user.is_staff:
-            request.session['cms_log_entries'] = LogEntry.objects.filter(user=request.user).count()
+            try:
+                request.cms_latest_entry = LogEntry.objects.filter(
+                    user=request.user,
+                    action_flag__in=(ADDITION, CHANGE)
+                ).only('pk').order_by('-pk')[0].pk
+            except IndexError:
+                request.cms_latest_entry = -1
         request.toolbar = CMSToolbar(request)
 
     def process_view(self, request, view_func, view_args, view_kwarg):
@@ -76,6 +83,7 @@ def process_view(self, request, view_func, view_args, view_kwarg):
 
     def process_response(self, request, response):
         from django.utils.cache import add_never_cache_headers
+
         found = False
         if hasattr(request, 'toolbar') and request.toolbar.edit_mode:
             found = True
@@ -86,10 +94,14 @@ def process_response(self, request, response):
         if found:
             add_never_cache_headers(response)
         if hasattr(request, 'user') and request.user.is_staff and response.status_code != 500:
-            count = LogEntry.objects.filter(user=request.user).count()
-            if request.session.get('cms_log_entries', 0) < count:
-                request.session['cms_log_entries'] = count
-                log = LogEntry.objects.filter(user=request.user)[0]
-                if log.action_flag == 1 or log.action_flag == 2:
-                    request.session['cms_log_latest'] = log.pk
+            try:
+                pk = LogEntry.objects.filter(
+                    user=request.user,
+                    action_flag__in=(ADDITION, CHANGE)
+                ).only('pk').order_by('-pk')[0].pk
+            except IndexError:
+                pk = -1
+            if hasattr(request, 'cms_latest_entry') and request.cms_latest_entry != pk:
+                log = LogEntry.objects.filter(user=request.user, action_flag__in=(ADDITION, CHANGE))[0]
+                request.session['cms_log_latest'] = log.pk
         return response


Test patch:
diff --git a/cms/tests/frontend.py b/cms/tests/frontend.py
--- a/cms/tests/frontend.py
+++ b/cms/tests/frontend.py
@@ -153,7 +153,7 @@ def reload_urls(self):
 class ToolbarBasicTests(CMSLiveTests):
 
     def setUp(self):
-        self.user = self._create_user('admin', True, True, True)
+        self.user = self.get_superuser()
         Site.objects.create(domain='example.org', name='example.org')
         self.base_url = self.live_server_url
         self.driver.implicitly_wait(2)