Просмотр исходного кода

12795 custom user model (#15005)

* 12795 users.User migration

* 12795 users.User migration

* 12795 review changes

* 12795 fix user model registration

* 12795 fix user model registration

* 12795 update migration

* 12795 update migration

* 12795 update migration

* 12795 add comment to migration db_table

* Tweak import to avoid class name collision

* 12795 add comment for _register_features requirement

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 2 лет назад
Родитель
Сommit
317bef6796

+ 3 - 3
netbox/netbox/navigation/menu.py

@@ -377,19 +377,19 @@ ADMIN_MENU = Menu(
             items=(
                 # Proxy model for auth.User
                 MenuItem(
-                    link=f'users:netboxuser_list',
+                    link=f'users:user_list',
                     link_text=_('Users'),
                     permissions=[f'auth.view_user'],
                     staff_only=True,
                     buttons=(
                         MenuItemButton(
-                            link=f'users:netboxuser_add',
+                            link=f'users:user_add',
                             title='Add',
                             icon_class='mdi mdi-plus-thick',
                             permissions=[f'auth.add_user']
                         ),
                         MenuItemButton(
-                            link=f'users:netboxuser_import',
+                            link=f'users:user_import',
                             title='Import',
                             icon_class='mdi mdi-upload',
                             permissions=[f'auth.add_user']

+ 4 - 0
netbox/netbox/settings.py

@@ -455,6 +455,8 @@ AUTHENTICATION_BACKENDS = [
     'netbox.authentication.ObjectPermissionBackend',
 ]
 
+AUTH_USER_MODEL = 'users.User'
+
 # Time zones
 USE_TZ = True
 
@@ -595,6 +597,8 @@ for param in dir(configuration):
 SOCIAL_AUTH_JSONFIELD_ENABLED = True
 SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'
 
+SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL
+
 #
 # Django Prometheus
 #

+ 1 - 1
netbox/templates/users/group.html

@@ -25,7 +25,7 @@
         <h5 class="card-header">{% trans "Users" %}</h5>
         <div class="list-group list-group-flush">
           {% for user in object.user_set.all %}
-            <a href="{% url 'users:netboxuser' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
+            <a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
           {% empty %}
             <div class="list-group-item text-muted">{% trans "None" %}</div>
           {% endfor %}

+ 1 - 1
netbox/templates/users/objectpermission.html

@@ -72,7 +72,7 @@
         <h5 class="card-header">{% trans "Assigned Users" %}</h5>
         <div class="list-group list-group-flush">
           {% for user in object.users.all %}
-            <a href="{% url 'users:netboxuser' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
+            <a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
           {% empty %}
             <div class="list-group-item text-muted">{% trans "None" %}</div>
           {% endfor %}

+ 1 - 1
netbox/templates/users/token.html

@@ -20,7 +20,7 @@
           <tr>
             <th scope="row">{% trans "User" %}</th>
             <td>
-              <a href="{% url 'users:netboxuser' pk=object.user.pk %}">{{ object.user }}</a>
+              <a href="{% url 'users:user' pk=object.user.pk %}">{{ object.user }}</a>
             </td>
           </tr>
           <tr>

+ 0 - 6
netbox/users/admin.py

@@ -1,6 +0,0 @@
-from django.contrib import admin
-from django.contrib.auth.models import Group, User
-
-# Unregister Django's built-in Group and User admin views
-admin.site.unregister(Group)
-admin.site.unregister(User)

+ 11 - 0
netbox/users/apps.py

@@ -6,3 +6,14 @@ class UsersConfig(AppConfig):
 
     def ready(self):
         import users.signals
+        from .models import NetBoxGroup, ObjectPermission, Token, User, UserConfig
+        from netbox.models.features import _register_features
+
+        # have to register these manually as the signal handler for class_prepared does
+        # not get registered until after these models are loaded. Any models defined in
+        # users.models should be registered here.
+        _register_features(NetBoxGroup)
+        _register_features(ObjectPermission)
+        _register_features(Token)
+        _register_features(User)
+        _register_features(UserConfig)

+ 2 - 2
netbox/users/forms/bulk_edit.py

@@ -17,7 +17,7 @@ __all__ = (
 
 class UserBulkEditForm(forms.Form):
     pk = forms.ModelMultipleChoiceField(
-        queryset=NetBoxUser.objects.all(),
+        queryset=User.objects.all(),
         widget=forms.MultipleHiddenInput
     )
     first_name = forms.CharField(
@@ -46,7 +46,7 @@ class UserBulkEditForm(forms.Form):
         label=_('Superuser status')
     )
 
-    model = NetBoxUser
+    model = User
     fieldsets = (
         (None, ('first_name', 'last_name', 'is_active', 'is_staff', 'is_superuser')),
     )

+ 1 - 1
netbox/users/forms/bulk_import.py

@@ -23,7 +23,7 @@ class GroupImportForm(CSVModelForm):
 class UserImportForm(CSVModelForm):
 
     class Meta:
-        model = NetBoxUser
+        model = User
         fields = (
             'username', 'first_name', 'last_name', 'email', 'password', 'is_staff',
             'is_active', 'is_superuser'

+ 2 - 2
netbox/users/forms/filtersets.py

@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
 
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms.mixins import SavedFiltersMixin
-from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
+from users.models import NetBoxGroup, User, ObjectPermission, Token
 from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm
 from utilities.forms.fields import DynamicModelMultipleChoiceField
 from utilities.forms.widgets import DateTimePicker
@@ -26,7 +26,7 @@ class GroupFilterForm(NetBoxModelFilterSetForm):
 
 
 class UserFilterForm(NetBoxModelFilterSetForm):
-    model = NetBoxUser
+    model = User
     fieldsets = (
         (None, ('q', 'filter_id',)),
         (_('Group'), ('group_id',)),

+ 1 - 1
netbox/users/forms/model_forms.py

@@ -198,7 +198,7 @@ class UserForm(forms.ModelForm):
     )
 
     class Meta:
-        model = NetBoxUser
+        model = User
         fields = [
             'username', 'first_name', 'last_name', 'email', 'groups', 'object_permissions',
             'is_active', 'is_staff', 'is_superuser',

+ 28 - 0
netbox/users/migrations/0001_squashed_0011.py

@@ -4,6 +4,7 @@ import django.contrib.postgres.fields
 import django.core.validators
 from django.db import migrations, models
 import django.db.models.deletion
+import users.models
 
 
 class Migration(migrations.Migration):
@@ -31,6 +32,33 @@ class Migration(migrations.Migration):
     ]
 
     operations = [
+        migrations.CreateModel(
+            name='User',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+                ('password', models.CharField(max_length=128)),
+                ('last_login', models.DateTimeField(blank=True, null=True)),
+                ('is_superuser', models.BooleanField(default=False)),
+                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()])),
+                ('first_name', models.CharField(blank=True, max_length=150)),
+                ('last_name', models.CharField(blank=True, max_length=150)),
+                ('email', models.EmailField(blank=True, max_length=254)),
+                ('is_staff', models.BooleanField(default=False)),
+                ('is_active', models.BooleanField(default=True)),
+                ('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
+                ('groups', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='auth.group')),
+                ('user_permissions', models.ManyToManyField(blank=True, related_name='user_set', related_query_name='user', to='auth.permission')),
+            ],
+            options={
+                'verbose_name': 'user',
+                'verbose_name_plural': 'users',
+                'db_table': 'auth_user',
+                'ordering': ('username',),
+            },
+            managers=[
+                ('objects', users.models.UserManager()),
+            ],
+        ),
         migrations.CreateModel(
             name='UserConfig',
             fields=[

+ 0 - 16
netbox/users/migrations/0002_squashed_0004.py

@@ -59,20 +59,4 @@ class Migration(migrations.Migration):
                 ('objects', django.contrib.auth.models.GroupManager()),
             ],
         ),
-        migrations.CreateModel(
-            name='NetBoxUser',
-            fields=[
-            ],
-            options={
-                'verbose_name': 'User',
-                'proxy': True,
-                'indexes': [],
-                'constraints': [],
-                'ordering': ('username',),
-            },
-            bases=('auth.user',),
-            managers=[
-                ('objects', django.contrib.auth.models.UserManager()),
-            ],
-        ),
     ]

+ 40 - 0
netbox/users/migrations/0005_alter_user_table.py

@@ -0,0 +1,40 @@
+# Generated by Django 5.0.1 on 2024-01-31 23:18
+
+from django.db import migrations
+
+
+def update_content_types(apps, schema_editor):
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+    # Delete the new ContentTypes effected by the new models in the users app
+    ContentType.objects.filter(app_label='users', model='user').delete()
+
+    # Update the app labels of the original ContentTypes for auth.User to ensure
+    # that any foreign key references are preserved
+    ContentType.objects.filter(app_label='auth', model='user').update(app_label='users')
+
+    netboxuser_ct = ContentType.objects.filter(app_label='users', model='netboxuser').first()
+    if netboxuser_ct:
+        user_ct = ContentType.objects.filter(app_label='users', model='user').first()
+        CustomField = apps.get_model('extras', 'CustomField')
+        CustomField.objects.filter(object_type_id=netboxuser_ct.id).update(object_type_id=user_ct.id)
+        netboxuser_ct.delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0002_squashed_0004'),
+    ]
+
+    operations = [
+        # 0001_squashed had model with db_table=auth_user - now we switch it
+        # to None to use the default Django resolution (users.user)
+        migrations.AlterModelTable(
+            name='user',
+            table=None,
+        ),
+        migrations.RunPython(
+            code=update_content_types,
+            reverse_code=migrations.RunPython.noop
+        ),
+    ]

+ 12 - 11
netbox/users/models.py

@@ -2,7 +2,10 @@ import binascii
 import os
 
 from django.conf import settings
-from django.contrib.auth.models import Group, GroupManager, User, UserManager
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import (
+    AbstractUser, Group, GroupManager, User as DjangoUser, UserManager as DjangoUserManager
+)
 from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ValidationError
 from django.core.validators import MinLengthValidator
@@ -23,9 +26,9 @@ from .constants import *
 
 __all__ = (
     'NetBoxGroup',
-    'NetBoxUser',
     'ObjectPermission',
     'Token',
+    'User',
     'UserConfig',
 )
 
@@ -34,7 +37,7 @@ __all__ = (
 # Proxies for Django's User and Group models
 #
 
-class NetBoxUserManager(UserManager.from_queryset(RestrictedQuerySet)):
+class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
     pass
 
 
@@ -42,20 +45,19 @@ class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
     pass
 
 
-class NetBoxUser(User):
+class User(AbstractUser):
     """
     Proxy contrib.auth.models.User for the UI
     """
-    objects = NetBoxUserManager()
+    objects = UserManager()
 
     class Meta:
-        proxy = True
         ordering = ('username',)
         verbose_name = _('user')
         verbose_name_plural = _('users')
 
     def get_absolute_url(self):
-        return reverse('users:netboxuser', args=[self.pk])
+        return reverse('users:user', args=[self.pk])
 
     def clean(self):
         super().clean()
@@ -91,7 +93,7 @@ class UserConfig(models.Model):
     This model stores arbitrary user-specific preferences in a JSON data structure.
     """
     user = models.OneToOneField(
-        to=User,
+        to=get_user_model(),
         on_delete=models.CASCADE,
         related_name='config'
     )
@@ -220,7 +222,6 @@ class UserConfig(models.Model):
 
 
 @receiver(post_save, sender=User)
-@receiver(post_save, sender=NetBoxUser)
 def create_userconfig(instance, created, raw=False, **kwargs):
     """
     Automatically create a new UserConfig when a new User is created. Skip this if importing a user from a fixture.
@@ -240,7 +241,7 @@ class Token(models.Model):
     It also supports setting an expiration time and toggling write ability.
     """
     user = models.ForeignKey(
-        to=User,
+        to=get_user_model(),
         on_delete=models.CASCADE,
         related_name='tokens'
     )
@@ -364,7 +365,7 @@ class ObjectPermission(models.Model):
         related_name='object_permissions'
     )
     users = models.ManyToManyField(
-        to=User,
+        to=get_user_model(),
         blank=True,
         related_name='object_permissions'
     )

+ 3 - 3
netbox/users/tables.py

@@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
 
 from account.tables import UserTokenTable
 from netbox.tables import NetBoxTable, columns
-from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
+from users.models import NetBoxGroup, User, ObjectPermission, Token
 
 __all__ = (
     'GroupTable',
@@ -49,7 +49,7 @@ class UserTable(NetBoxTable):
     )
 
     class Meta(NetBoxTable.Meta):
-        model = NetBoxUser
+        model = User
         fields = (
             'pk', 'id', 'username', 'first_name', 'last_name', 'email', 'groups', 'is_active', 'is_staff',
             'is_superuser', 'last_login',
@@ -103,7 +103,7 @@ class ObjectPermissionTable(NetBoxTable):
     )
     users = columns.ManyToManyColumn(
         verbose_name=_('Users'),
-        linkify_item=('users:netboxuser', {'pk': tables.A('pk')})
+        linkify_item=('users:user', {'pk': tables.A('pk')})
     )
     groups = columns.ManyToManyColumn(
         verbose_name=_('Groups'),

+ 5 - 5
netbox/users/tests/test_views.py

@@ -15,7 +15,7 @@ class UserTestCase(
     ViewTestCases.BulkEditObjectsViewTestCase,
     ViewTestCases.BulkDeleteObjectsViewTestCase,
 ):
-    model = NetBoxUser
+    model = User
     maxDiff = None
     validation_excluded_fields = ['password']
 
@@ -27,11 +27,11 @@ class UserTestCase(
     def setUpTestData(cls):
 
         users = (
-            NetBoxUser(username='username1', first_name='first1', last_name='last1', email='user1@foo.com', password='pass1xxx'),
-            NetBoxUser(username='username2', first_name='first2', last_name='last2', email='user2@foo.com', password='pass2xxx'),
-            NetBoxUser(username='username3', first_name='first3', last_name='last3', email='user3@foo.com', password='pass3xxx'),
+            User(username='username1', first_name='first1', last_name='last1', email='user1@foo.com', password='pass1xxx'),
+            User(username='username2', first_name='first2', last_name='last2', email='user2@foo.com', password='pass2xxx'),
+            User(username='username3', first_name='first3', last_name='last3', email='user3@foo.com', password='pass3xxx'),
         )
-        NetBoxUser.objects.bulk_create(users)
+        User.objects.bulk_create(users)
 
         cls.form_data = {
             'username': 'usernamex',

+ 6 - 6
netbox/users/urls.py

@@ -15,12 +15,12 @@ urlpatterns = [
     path('tokens/<int:pk>/', include(get_model_urls('users', 'token'))),
 
     # Users
-    path('users/', views.UserListView.as_view(), name='netboxuser_list'),
-    path('users/add/', views.UserEditView.as_view(), name='netboxuser_add'),
-    path('users/edit/', views.UserBulkEditView.as_view(), name='netboxuser_bulk_edit'),
-    path('users/import/', views.UserBulkImportView.as_view(), name='netboxuser_import'),
-    path('users/delete/', views.UserBulkDeleteView.as_view(), name='netboxuser_bulk_delete'),
-    path('users/<int:pk>/', include(get_model_urls('users', 'netboxuser'))),
+    path('users/', views.UserListView.as_view(), name='user_list'),
+    path('users/add/', views.UserEditView.as_view(), name='user_add'),
+    path('users/edit/', views.UserBulkEditView.as_view(), name='user_bulk_edit'),
+    path('users/import/', views.UserBulkImportView.as_view(), name='user_import'),
+    path('users/delete/', views.UserBulkDeleteView.as_view(), name='user_bulk_delete'),
+    path('users/<int:pk>/', include(get_model_urls('users', 'user'))),
 
     # Groups
     path('groups/', views.GroupListView.as_view(), name='netboxgroup_list'),

+ 11 - 11
netbox/users/views.py

@@ -5,7 +5,7 @@ from extras.tables import ObjectChangeTable
 from netbox.views import generic
 from utilities.views import register_model_view
 from . import filtersets, forms, tables
-from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
+from .models import NetBoxGroup, User, ObjectPermission, Token
 
 
 #
@@ -56,15 +56,15 @@ class TokenBulkDeleteView(generic.BulkDeleteView):
 #
 
 class UserListView(generic.ObjectListView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     filterset = filtersets.UserFilterSet
     filterset_form = forms.UserFilterForm
     table = tables.UserTable
 
 
-@register_model_view(NetBoxUser)
+@register_model_view(User)
 class UserView(generic.ObjectView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     template_name = 'users/user.html'
 
     def get_extra_context(self, request, instance):
@@ -76,31 +76,31 @@ class UserView(generic.ObjectView):
         }
 
 
-@register_model_view(NetBoxUser, 'edit')
+@register_model_view(User, 'edit')
 class UserEditView(generic.ObjectEditView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     form = forms.UserForm
 
 
-@register_model_view(NetBoxUser, 'delete')
+@register_model_view(User, 'delete')
 class UserDeleteView(generic.ObjectDeleteView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
 
 
 class UserBulkEditView(generic.BulkEditView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     filterset = filtersets.UserFilterSet
     table = tables.UserTable
     form = forms.UserBulkEditForm
 
 
 class UserBulkImportView(generic.BulkImportView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     model_form = forms.UserImportForm
 
 
 class UserBulkDeleteView(generic.BulkDeleteView):
-    queryset = NetBoxUser.objects.all()
+    queryset = User.objects.all()
     filterset = filtersets.UserFilterSet
     table = tables.UserTable