Skip to content

12795 custom user model #15005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions netbox/netbox/navigation/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,19 +377,19 @@
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']
Expand Down
4 changes: 4 additions & 0 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,8 @@ def _setting(name, default=None):
'netbox.authentication.ObjectPermissionBackend',
]

AUTH_USER_MODEL = 'users.User'

# Time zones
USE_TZ = True

Expand Down Expand Up @@ -592,6 +594,8 @@ def _setting(name, default=None):
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = 'users.utils.clean_username'

SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL

#
# Django Prometheus
#
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/group.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h5 class="card-header">{% trans "Group" %}</h5>
<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 %}
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/objectpermission.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ <h5 class="card-header">{% trans "Object Types" %}</h5>
<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 %}
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/users/token.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h5 class="card-header">{% trans "Token" %}</h5>
<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>
Expand Down
6 changes: 0 additions & 6 deletions netbox/users/admin.py

This file was deleted.

11 changes: 11 additions & 0 deletions netbox/users/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions netbox/users/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

class UserBulkEditForm(forms.Form):
pk = forms.ModelMultipleChoiceField(
queryset=NetBoxUser.objects.all(),
queryset=User.objects.all(),
widget=forms.MultipleHiddenInput
)
first_name = forms.CharField(
Expand Down Expand Up @@ -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')),
)
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Meta:
class UserImportForm(CSVModelForm):

class Meta:
model = NetBoxUser
model = User
fields = (
'username', 'first_name', 'last_name', 'email', 'password', 'is_staff',
'is_active', 'is_superuser'
Expand Down
4 changes: 2 additions & 2 deletions netbox/users/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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
Expand All @@ -26,7 +26,7 @@ class GroupFilterForm(NetBoxModelFilterSetForm):


class UserFilterForm(NetBoxModelFilterSetForm):
model = NetBoxUser
model = User
fieldsets = (
(None, ('q', 'filter_id',)),
(_('Group'), ('group_id',)),
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions netbox/users/migrations/0001_squashed_0011.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import users.models


class Migration(migrations.Migration):
Expand Down Expand Up @@ -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=[
Expand Down
16 changes: 0 additions & 16 deletions netbox/users/migrations/0002_squashed_0004.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 40 additions & 0 deletions netbox/users/migrations/0005_alter_user_table.py
Original file line number Diff line number Diff line change
@@ -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
),
]
23 changes: 12 additions & 11 deletions netbox/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
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
Expand All @@ -23,9 +26,9 @@

__all__ = (
'NetBoxGroup',
'NetBoxUser',
'ObjectPermission',
'Token',
'User',
'UserConfig',
)

Expand All @@ -34,28 +37,27 @@
# Proxies for Django's User and Group models
#

class NetBoxUserManager(UserManager.from_queryset(RestrictedQuerySet)):
class UserManager(DjangoUserManager.from_queryset(RestrictedQuerySet)):
pass


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()
Expand Down Expand Up @@ -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'
)
Expand Down Expand Up @@ -220,7 +222,6 @@ def clear(self, path, commit=False):


@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.
Expand All @@ -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'
)
Expand Down Expand Up @@ -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'
)
Expand Down
6 changes: 3 additions & 3 deletions netbox/users/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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'),
Expand Down
10 changes: 5 additions & 5 deletions netbox/users/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class UserTestCase(
ViewTestCases.BulkEditObjectsViewTestCase,
ViewTestCases.BulkDeleteObjectsViewTestCase,
):
model = NetBoxUser
model = User
maxDiff = None
validation_excluded_fields = ['password']

Expand All @@ -27,11 +27,11 @@ def _get_queryset(self):
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',
Expand Down
Loading