Repository: django-cms/django-cms

Issue:
PageUserAdmin doesn't work with custom user model
We have a custom user model that has a `full_name` field instead of `first_name` and `last_name`.

When we set `CMS_PERMISSION = True`, the CMS breaks with this error:

```
File "/home/chris/dev/dfidbeamexchange/django/website/.ve/local/lib/python2.7/site-packages/django/contrib/admin/validation.py", line 251, in validate_list_display
% (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
```

ImproperlyConfigured: PageUserAdmin.list_display[1], 'first_name' is not a callable or an attribute of 'PageUserAdmin' or found in the model 'PageUser'.

PageUserAdmin hard-codes the field list, which breaks it for us:

```
class PageUserAdmin(UserAdmin, GenericCmsPermissionAdmin):
    form = PageUserForm
    add_form = PageUserForm
    model = PageUser

    list_display = ('email', 'first_name', 'last_name', 'created_by')
```

What should we do here? Unregister `PageUserAdmin` and replace it with a custom one?



Discussion:
Override `get_list_display`? https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_list_display

Unfortunately that's not enough. It crashes during validating the models, which happens on **import** of `cms.admin.useradmin` (during `autodiscover`), not when the page is accessed. So we can't subclass the `PageUserAdmin` because we can't import it, or allow it to be imported.

So, maybe this one https://docs.djangoproject.com/en/1.6/topics/auth/customizing/#django.contrib.auth.models.CustomUser.REQUIRED_FIELDS together with my first advice.

@mmarzantowicz so is your advice that we should submit a pull request to Django-CMS to change the list_display to include the REQUIRED_FIELDS instead of hard-coding the list of fields? Or did I misunderstand?

This might be the best you can do. Quick fix might be not to use `first_name` and `last_name` but `full_name` instead. Can you play with it and see if it works before making PR? I do not currently have any installation with custom User model to play with.


Resolving patch:
diff --git a/cms/admin/permissionadmin.py b/cms/admin/permissionadmin.py
--- a/cms/admin/permissionadmin.py
+++ b/cms/admin/permissionadmin.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 from copy import deepcopy
-
 from django.contrib import admin
+from django.contrib.admin import site
+from django.contrib.auth.admin import UserAdmin
 from django.utils.translation import ugettext as _
 
 from cms.admin.forms import GlobalPagePermissionAdminForm, PagePermissionInlineAdminForm, ViewRestrictionInlineAdminForm
@@ -14,6 +15,11 @@
 
 PERMISSION_ADMIN_INLINES = []
 
+user_model = get_user_model()
+admin_class = UserAdmin
+for model, admin_instance in site._registry.items():
+    if model == user_model:
+        admin_class = admin_instance.__class__
 
 class TabularInline(admin.TabularInline):
     pass
@@ -116,8 +122,10 @@ class GlobalPagePermissionAdmin(admin.ModelAdmin):
     list_filter = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions']
 
     form = GlobalPagePermissionAdminForm
-
-    search_fields = ('user__'+get_user_model().USERNAME_FIELD, 'user__first_name', 'user__last_name', 'group__name')
+    search_fields = []
+    for field in admin_class.search_fields:
+        search_fields.append("user__%s" % field)
+    search_fields.append('group__name')
 
     exclude = []
 
diff --git a/cms/admin/useradmin.py b/cms/admin/useradmin.py
--- a/cms/admin/useradmin.py
+++ b/cms/admin/useradmin.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from django.utils.translation import ugettext as _
 from django.contrib import admin
 
 from cms.admin.forms import PageUserForm, PageUserGroupForm
@@ -10,44 +9,19 @@
 from cms.utils.compat.forms import UserAdmin
 from cms.utils.conf import get_cms_setting
 from cms.utils.permissions import get_subordinate_users
+from django.contrib.admin import site
 
-class PageUserAdmin(UserAdmin, GenericCmsPermissionAdmin):
+user_model = get_user_model()
+admin_class = UserAdmin
+for model, admin_instance in site._registry.items():
+    if model == user_model:
+        admin_class = admin_instance.__class__
+
+class PageUserAdmin(admin_class, GenericCmsPermissionAdmin):
     form = PageUserForm
     add_form = PageUserForm
     model = PageUser
     
-    list_display = ('email', 'first_name', 'last_name', 'created_by')
-
-    if get_user_model().USERNAME_FIELD != 'email':
-        list_display += (get_user_model().USERNAME_FIELD,)
-    
-    # get_fieldsets method may add fieldsets depending on user
-    fieldsets = [
-        (None, {'fields': (get_user_model().USERNAME_FIELD, ('password1', 'password2'), 'notify_user')}),
-    ]
-
-    if get_user_model().USERNAME_FIELD != 'email':
-        fieldsets.append((_('User details'), {'fields': (('first_name', 'last_name'), 'email')}))
-    else:
-        fieldsets.append((_('User details'), {'fields': (('first_name', 'last_name'))}))
-        
-    fieldsets.append((_('Groups'), {'fields': ('groups',)}))
-    
-    add_fieldsets = fieldsets
-
-    ordering = ('last_name', 'first_name', 'email')
-
-    if get_user_model().USERNAME_FIELD != 'email':
-        ordering = (get_user_model().USERNAME_FIELD,) + ordering
-    
-    def get_fieldsets(self, request, obj=None):
-        fieldsets = self.update_permission_fieldsets(request, obj)
-        
-        if not '/add' in request.path_info:
-            fieldsets[0] = (None, {'fields': (get_user_model().USERNAME_FIELD, 'notify_user')})
-            fieldsets.append((_('Password'), {'fields': ('password1', 'password2'), 'classes': ('collapse',)}))
-        return fieldsets
-    
     def queryset(self, request):
         qs = super(PageUserAdmin, self).queryset(request)
         try:
@@ -55,9 +29,7 @@ def queryset(self, request):
             return qs.filter(pk__in=user_id_set)
         except NoPermissionsException:
             return self.model.objects.get_empty_query_set()
-    
-    def add_view(self, request):
-        return super(UserAdmin, self).add_view(request)
+
     
 class PageUserGroupAdmin(admin.ModelAdmin, GenericCmsPermissionAdmin):
     form = PageUserGroupForm


Test patch:
diff --git a/cms/test_utils/project/customuserapp/admin.py b/cms/test_utils/project/customuserapp/admin.py
--- a/cms/test_utils/project/customuserapp/admin.py
+++ b/cms/test_utils/project/customuserapp/admin.py
@@ -7,6 +7,7 @@
 
 if getattr(OriginalUser._meta, 'swapped', False):
     class UserAdmin(OriginalUserAdmin):
-        pass
+        list_display = ('username', 'email', 'get_full_name', 'is_staff')
+        search_fields = ('username', 'email',)
 
-    admin.site.register(get_user_model(), UserAdmin)
\ No newline at end of file
+    admin.site.register(get_user_model(), UserAdmin)
diff --git a/cms/test_utils/project/customuserapp/models.py b/cms/test_utils/project/customuserapp/models.py
--- a/cms/test_utils/project/customuserapp/models.py
+++ b/cms/test_utils/project/customuserapp/models.py
@@ -1,8 +1,83 @@
 # -*- coding: utf-8 -*-
+from django.core.mail import send_mail
 from django.db import models
+from django.utils import timezone
+from django.utils.http import urlquote
+
 try:
-    from django.contrib.auth.models import AbstractUser
-    class User(AbstractUser):
+    import re
+    from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager
+    from django.core import validators
+    from django.utils.translation import ugettext_lazy as _
+
+    class CustomUserManager(UserManager):
+
+        def _create_user(self, username, email, password,
+                         is_staff, is_superuser, **extra_fields):
+            """
+            Creates and saves a User with the given username, email and password.
+            """
+            now = timezone.now()
+            if not username:
+                raise ValueError('The given username must be set')
+            email = self.normalize_email(email)
+            user = self.model(username=username, email=email,
+                              is_staff=is_staff, is_active=True,
+                              is_superuser=is_superuser, last_login=now,
+                              **extra_fields)
+            user.set_password(password)
+            user.save(using=self._db)
+            return user
+
+    class User(AbstractBaseUser, PermissionsMixin):
+        """
+        An abstract base class implementing a fully featured User model with
+        admin-compliant permissions.
+
+        Username, password and email are required. Other fields are optional.
+        """
+        username = models.CharField(_('username'), max_length=30, unique=True,
+            help_text=_('Required. 30 characters or fewer. Letters, numbers and '
+                        '@/./+/-/_ characters'),
+            validators=[
+                validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
+            ])
+        email = models.EmailField(_('email address'), blank=True)
+        is_staff = models.BooleanField(_('staff status'), default=False,
+            help_text=_('Designates whether the user can log into this admin '
+                        'site.'))
+        is_active = models.BooleanField(_('active'), default=True,
+            help_text=_('Designates whether this user should be treated as '
+                        'active. Unselect this instead of deleting accounts.'))
         my_new_field = models.IntegerField(null=True, blank=True, default=42)
+
+        objects = CustomUserManager()
+
+        USERNAME_FIELD = 'username'
+        REQUIRED_FIELDS = ['email']
+
+        class Meta:
+            verbose_name = _('user')
+            verbose_name_plural = _('users')
+
+        def get_absolute_url(self):
+            return "/users/%s/" % urlquote(self.username)
+
+        def get_full_name(self):
+            """
+            Returns the first_name plus the last_name, with a space in between.
+            """
+            return "A user called %s" % self.username
+
+        def get_short_name(self):
+            "Returns the short name for the user."
+            return self.username
+
+        def email_user(self, subject, message, from_email=None):
+            """
+            Sends an email to this User.
+            """
+            send_mail(subject, message, from_email, [self.email])
 except ImportError:
+    raise
     from django.contrib.auth.models import User  # nopyflakes
diff --git a/cms/test_utils/testcases.py b/cms/test_utils/testcases.py
--- a/cms/test_utils/testcases.py
+++ b/cms/test_utils/testcases.py
@@ -94,7 +94,6 @@ def create_fixtures(self):
         pass
 
     def _post_teardown(self):
-        # Needed to clean the menu keys cache, see menu.menu_pool.clear()
         menu_pool.clear()
         cache.clear()
         super(BaseCMSTestCase, self)._post_teardown()
diff --git a/cms/tests/toolbar.py b/cms/tests/toolbar.py
--- a/cms/tests/toolbar.py
+++ b/cms/tests/toolbar.py
@@ -349,6 +349,19 @@ def test_page_edit_redirect(self):
         response = self.client.post(url, {'pk': page1.pk, 'model': 'cms.page'})
         self.assertEqual(response.content.decode('utf-8'), '')
 
+    def get_username(self, user=None, default=''):
+        user = user or self.request.user
+        try:
+            name = user.get_full_name()
+            if name:
+                return name
+            elif DJANGO_1_4:
+                return user.username
+            else:
+                return user.get_username()
+        except (AttributeError, NotImplementedError):
+            return default
+
     def test_toolbar_logout(self):
         '''
         Tests that the Logout menu item includes the user's full name, if the
@@ -375,10 +388,7 @@ def test_toolbar_logout(self):
             response = self.client.get(page.get_absolute_url('en') + '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
             toolbar = response.context['request'].toolbar
             admin_menu = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
-            if DJANGO_1_4:
-                self.assertTrue(admin_menu.find_first(AjaxItem, name=_(u'Logout %s') % superuser.username))
-            else:
-                self.assertTrue(admin_menu.find_first(AjaxItem, name=_(u'Logout %s') % superuser.get_username()))
+            self.assertTrue(admin_menu.find_first(AjaxItem, name=_(u'Logout %s') % self.get_username(superuser)))
 
         #
         # Test that the logout shows the logged-in user's name, if it was
@@ -388,13 +398,12 @@ def test_toolbar_logout(self):
         superuser.last_name = 'User'
         superuser.save()
         # Sanity check...
-        self.assertEqual('Super User', superuser.get_full_name())
         self.get_page_request(page, superuser, '/')
         with self.login_user_context(superuser):
             response = self.client.get(page.get_absolute_url('en') + '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
             toolbar = response.context['request'].toolbar
             admin_menu = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
-            self.assertTrue(admin_menu.find_first(AjaxItem, name=_(u'Logout %s') % superuser.get_full_name()))
+            self.assertTrue(admin_menu.find_first(AjaxItem, name=_(u'Logout %s') % self.get_username(superuser)))
 
     def test_toolbar_logout_redirect(self):
         """
@@ -422,15 +431,11 @@ def test_toolbar_logout_redirect(self):
         page4 = page4.get_public_object()
         self.get_page_request(page4, superuser, '/')
 
-        if DJANGO_1_4:
-            menu_name = _(u'Logout %s') % superuser.username
-        else:
-            menu_name = _(u'Logout %s') % superuser.get_username()
-
         with self.login_user_context(superuser):
             # Published page, no redirect
             response = self.client.get(page1.get_absolute_url('en') + '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
             toolbar = response.context['request'].toolbar
+            menu_name = _(u'Logout %s') % self.get_username(superuser)
             admin_menu = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
             self.assertTrue(admin_menu.find_first(AjaxItem, name=menu_name).item.on_success)
 
@@ -438,6 +443,7 @@ def test_toolbar_logout_redirect(self):
             response = self.client.get(page2.get_absolute_url('en') + '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
             toolbar = response.context['request'].toolbar
             admin_menu = toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
+
             self.assertEquals(admin_menu.find_first(AjaxItem, name=menu_name).item.on_success, '/')
 
             # Published page with login restrictions, redirect