diff --git a/netbox/netbox/ui/attrs.py b/netbox/netbox/ui/attrs.py index 3c3dd4061..b4e2a61ee 100644 --- a/netbox/netbox/ui/attrs.py +++ b/netbox/netbox/ui/attrs.py @@ -10,6 +10,7 @@ __all__ = ( 'BooleanAttr', 'ChoiceAttr', 'ColorAttr', + 'DateTimeAttr', 'GPSCoordinatesAttr', 'GenericForeignKeyAttr', 'ImageAttr', @@ -367,6 +368,26 @@ class GPSCoordinatesAttr(ObjectAttribute): }) +class DateTimeAttr(ObjectAttribute): + """ + A date or datetime attribute. + + Parameters: + spec (str): Controls the rendering format. Use 'date' for date-only rendering, + or 'seconds'/'minutes' for datetime rendering with the given precision. + """ + template_name = 'ui/attrs/datetime.html' + + def __init__(self, *args, spec='seconds', **kwargs): + super().__init__(*args, **kwargs) + self.spec = spec + + def get_context(self, obj, context): + return { + 'spec': self.spec, + } + + class TimezoneAttr(ObjectAttribute): """ A timezone value. Includes the numeric offset from UTC. diff --git a/netbox/templates/ui/attrs/datetime.html b/netbox/templates/ui/attrs/datetime.html new file mode 100644 index 000000000..b5b968d4e --- /dev/null +++ b/netbox/templates/ui/attrs/datetime.html @@ -0,0 +1 @@ +{% load helpers %}{% if spec == 'date' %}{{ value|isodate }}{% else %}{{ value|isodatetime:spec }}{% endif %} diff --git a/netbox/templates/users/attrs/full_name.html b/netbox/templates/users/attrs/full_name.html new file mode 100644 index 000000000..3bb006313 --- /dev/null +++ b/netbox/templates/users/attrs/full_name.html @@ -0,0 +1 @@ +{% load helpers %}{{ object.get_full_name|placeholder }} diff --git a/netbox/templates/users/group.html b/netbox/templates/users/group.html index 9d4561c65..511d04559 100644 --- a/netbox/templates/users/group.html +++ b/netbox/templates/users/group.html @@ -1,60 +1,3 @@ {% extends 'generic/object.html' %} -{% load i18n %} -{% load helpers %} -{% load render_table from django_tables2 %} - -{% block title %}{% trans "Group" %} {{ object.name }}{% endblock %} {% block subtitle %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Group" %}

- - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
-
-
-
-
-

{% trans "Users" %}

-
- {% for user in object.users.all %} - {{ user }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Assigned Permissions" %}

-
- {% for perm in object.object_permissions.all %} - {{ perm }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Owner Membership" %}

-
- {% for owner in object.owners.all %} - {{ owner }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-
-{% endblock %} diff --git a/netbox/templates/users/objectpermission.html b/netbox/templates/users/objectpermission.html index 23f5ef866..b7218557d 100644 --- a/netbox/templates/users/objectpermission.html +++ b/netbox/templates/users/objectpermission.html @@ -1,93 +1,5 @@ {% extends 'generic/object.html' %} {% load i18n %} -{% load helpers %} -{% load render_table from django_tables2 %} {% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %} - {% block subtitle %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Permission" %}

- - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
{% trans "Enabled" %}{% checkmark object.enabled %}
-
-
-

{% trans "Actions" %}

- - - - - - - - - - - - - - - - - -
{% trans "View" %}{% checkmark object.can_view %}
{% trans "Add" %}{% checkmark object.can_add %}
{% trans "Change" %}{% checkmark object.can_change %}
{% trans "Delete" %}{% checkmark object.can_delete %}
-
-
-

{% trans "Constraints" %}

-
- {% if object.constraints %} -
{{ object.constraints|json }}
- {% else %} - None - {% endif %} -
-
-
-
-
-

{% trans "Object Types" %}

-
    - {% for user in object.object_types.all %} -
  • {{ user }}
  • - {% endfor %} -
-
-
-

{% trans "Assigned Users" %}

-
- {% for user in object.users.all %} - {{ user }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Assigned Groups" %}

-
- {% for group in object.groups.all %} - {{ group }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-
-{% endblock %} diff --git a/netbox/templates/users/owner.html b/netbox/templates/users/owner.html index 3e9d1c125..d592ea276 100644 --- a/netbox/templates/users/owner.html +++ b/netbox/templates/users/owner.html @@ -11,50 +11,3 @@ {% endblock %} {% block subtitle %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "Owner" %}

- - - - - - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Group" %}{{ object.group|linkify|placeholder }}
{% trans "Description" %}{{ object.description|placeholder }}
-
-
-

{% trans "Groups" %}

-
- {% for group in object.user_groups.all %} - {{ group }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Users" %}

-
- {% for user in object.users.all %} - {{ user }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-
- {% include 'inc/panels/related_objects.html' with filter_name='owner_id' %} -
-
-{% endblock %} diff --git a/netbox/templates/users/ownergroup.html b/netbox/templates/users/ownergroup.html index c45da792a..511d04559 100644 --- a/netbox/templates/users/ownergroup.html +++ b/netbox/templates/users/ownergroup.html @@ -1,46 +1,3 @@ {% extends 'generic/object.html' %} -{% load i18n %} -{% load helpers %} -{% load render_table from django_tables2 %} {% block subtitle %}{% endblock %} - -{% block extra_controls %} - {% if perms.users.add_owner %} - - {% trans "Add Owner" %} - - {% endif %} -{% endblock extra_controls %} - -{% block content %} -
-
-
-

{% trans "Group" %}

- - - - - - - - - -
{% trans "Name" %}{{ object.name }}
{% trans "Description" %}{{ object.description|placeholder }}
-
-
-
-
-

{% trans "Members" %}

-
- {% for owner in object.members.all %} - {{ owner }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-
-{% endblock %} diff --git a/netbox/templates/users/panels/object_types.html b/netbox/templates/users/panels/object_types.html new file mode 100644 index 000000000..838563cf6 --- /dev/null +++ b/netbox/templates/users/panels/object_types.html @@ -0,0 +1,11 @@ +{% load i18n %} +
+

{% trans "Object Types" %}

+ +
diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index efcc743f5..511d04559 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -1,85 +1,3 @@ {% extends 'generic/object.html' %} -{% load i18n %} - -{% block title %}{% trans "User" %} {{ object.username }}{% endblock %} {% block subtitle %}{% endblock %} - -{% block content %} -
-
-
-

{% trans "User" %}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans "Username" %}{{ object.username }}
{% trans "Full Name" %}{{ object.get_full_name|placeholder }}
{% trans "Email" %}{{ object.email|placeholder }}
{% trans "Account Created" %}{{ object.date_joined|isodate }}
{% trans "Last Login" %}{{ object.last_login|isodatetime:"minutes"|placeholder }}
{% trans "Active" %}{% checkmark object.is_active %}
{% trans "Superuser" %}{% checkmark object.is_superuser %}
-
-
-
-
-

{% trans "Assigned Groups" %}

-
- {% for group in object.groups.all %} - {{ group }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Assigned Permissions" %}

-
- {% for perm in object.object_permissions.all %} - {{ perm }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-

{% trans "Owner Membership" %}

-
- {% for owner in object.owners.all %} - {{ owner }} - {% empty %} -
{% trans "None" %}
- {% endfor %} -
-
-
-
- {% if perms.core.view_objectchange %} -
-
- {% include 'users/inc/user_activity.html' with user=object table=changelog_table %} -
-
- {% endif %} -{% endblock %} diff --git a/netbox/users/ui/panels.py b/netbox/users/ui/panels.py index 92a4dd46a..9ee27f4a8 100644 --- a/netbox/users/ui/panels.py +++ b/netbox/users/ui/panels.py @@ -23,3 +23,38 @@ class TokenExamplePanel(panels.Panel): actions = [ actions.CopyContent('token-example') ] + + +class UserPanel(panels.ObjectAttributesPanel): + username = attrs.TextAttr('username') + full_name = attrs.TemplatedAttr( + 'get_full_name', + label=_('Full name'), + template_name='users/attrs/full_name.html', + ) + email = attrs.TextAttr('email') + date_joined = attrs.DateTimeAttr('date_joined', label=_('Account created'), spec='date') + last_login = attrs.DateTimeAttr('last_login', label=_('Last login'), spec='minutes') + is_active = attrs.BooleanAttr('is_active', label=_('Active')) + is_superuser = attrs.BooleanAttr('is_superuser', label=_('Superuser')) + + +class ObjectPermissionPanel(panels.ObjectAttributesPanel): + name = attrs.TextAttr('name') + description = attrs.TextAttr('description') + enabled = attrs.BooleanAttr('enabled') + + +class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel): + title = _('Actions') + + can_view = attrs.BooleanAttr('can_view', label=_('View')) + can_add = attrs.BooleanAttr('can_add', label=_('Add')) + can_change = attrs.BooleanAttr('can_change', label=_('Change')) + can_delete = attrs.BooleanAttr('can_delete', label=_('Delete')) + + +class OwnerPanel(panels.ObjectAttributesPanel): + name = attrs.TextAttr('name') + group = attrs.RelatedObjectAttr('group', linkify=True) + description = attrs.TextAttr('description') diff --git a/netbox/users/views.py b/netbox/users/views.py index 44f311615..59ebe518f 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -1,9 +1,18 @@ from django.db.models import Count +from django.utils.translation import gettext_lazy as _ from core.models import ObjectChange from core.tables import ObjectChangeTable from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename -from netbox.ui import layout +from netbox.ui import actions, layout +from netbox.ui.panels import ( + ContextTablePanel, + JSONPanel, + ObjectsTablePanel, + OrganizationalObjectPanel, + RelatedObjectsPanel, + TemplatePanel, +) from netbox.views import generic from users.ui import panels from utilities.query import count_related @@ -86,7 +95,39 @@ class UserListView(generic.ObjectListView): @register_model_view(User) class UserView(generic.ObjectView): queryset = User.objects.all() - template_name = 'users/user.html' + layout = layout.SimpleLayout( + left_panels=[ + panels.UserPanel(), + ], + right_panels=[ + ObjectsTablePanel( + 'users.Group', title=_('Assigned Groups'), filters={'user_id': lambda ctx: ctx['object'].pk} + ), + ObjectsTablePanel( + 'users.ObjectPermission', + title=_('Assigned Permissions'), + filters={'user_id': lambda ctx: ctx['object'].pk}, + ), + ObjectsTablePanel( + 'users.Owner', title=_('Owner Membership'), filters={'user_id': lambda ctx: ctx['object'].pk} + ), + ], + bottom_panels=[ + ContextTablePanel( + 'changelog_table', + title=_('Recent Activity'), + actions=[ + actions.LinkAction( + view_name='core:objectchange_list', + url_params={'user_id': lambda ctx: ctx['object'].pk}, + label=_('View All'), + button_icon='arrow-right-thick', + permissions=['core.view_objectchange'], + ), + ], + ), + ], + ) def get_extra_context(self, request, instance): changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20] @@ -154,7 +195,22 @@ class GroupListView(generic.ObjectListView): @register_model_view(Group) class GroupView(generic.ObjectView): queryset = Group.objects.all() - template_name = 'users/group.html' + layout = layout.SimpleLayout( + left_panels=[ + OrganizationalObjectPanel(), + ], + right_panels=[ + ObjectsTablePanel('users.User', filters={'group_id': lambda ctx: ctx['object'].pk}), + ObjectsTablePanel( + 'users.ObjectPermission', + title=_('Assigned Permissions'), + filters={'group_id': lambda ctx: ctx['object'].pk}, + ), + ObjectsTablePanel( + 'users.Owner', title=_('Owner Membership'), filters={'user_group_id': lambda ctx: ctx['object'].pk} + ), + ], + ) @register_model_view(Group, 'add', detail=False) @@ -212,7 +268,22 @@ class ObjectPermissionListView(generic.ObjectListView): @register_model_view(ObjectPermission) class ObjectPermissionView(generic.ObjectView): queryset = ObjectPermission.objects.all() - template_name = 'users/objectpermission.html' + layout = layout.SimpleLayout( + left_panels=[ + panels.ObjectPermissionPanel(), + panels.ObjectPermissionActionsPanel(), + JSONPanel('constraints', title=_('Constraints')), + ], + right_panels=[ + TemplatePanel('users/panels/object_types.html'), + ObjectsTablePanel( + 'users.User', title=_('Assigned Users'), filters={'permission_id': lambda ctx: ctx['object'].pk} + ), + ObjectsTablePanel( + 'users.Group', title=_('Assigned Groups'), filters={'permission_id': lambda ctx: ctx['object'].pk} + ), + ], + ) @register_model_view(ObjectPermission, 'add', detail=False) @@ -255,7 +326,7 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView): @register_model_view(OwnerGroup, 'list', path='', detail=False) class OwnerGroupListView(generic.ObjectListView): queryset = OwnerGroup.objects.annotate( - owner_count=count_related(Owner, 'group') + owner_count=count_related(Owner, 'group') ) filterset = filtersets.OwnerGroupFilterSet filterset_form = forms.OwnerGroupFilterForm @@ -263,14 +334,26 @@ class OwnerGroupListView(generic.ObjectListView): @register_model_view(OwnerGroup) -class OwnerGroupView(GetRelatedModelsMixin, generic.ObjectView): +class OwnerGroupView(generic.ObjectView): queryset = OwnerGroup.objects.all() - template_name = 'users/ownergroup.html' - - def get_extra_context(self, request, instance): - return { - 'related_models': self.get_related_models(request, instance), - } + layout = layout.SimpleLayout( + left_panels=[ + OrganizationalObjectPanel(), + ], + right_panels=[ + ObjectsTablePanel( + 'users.Owner', + filters={'group_id': lambda ctx: ctx['object'].pk}, + title=_('Members'), + actions=[ + actions.AddObject( + 'users.Owner', + url_params={'group': lambda ctx: ctx['object'].pk}, + ), + ], + ), + ], + ) @register_model_view(OwnerGroup, 'add', detail=False) @@ -326,7 +409,16 @@ class OwnerListView(generic.ObjectListView): @register_model_view(Owner) class OwnerView(GetRelatedModelsMixin, generic.ObjectView): queryset = Owner.objects.all() - template_name = 'users/owner.html' + layout = layout.SimpleLayout( + left_panels=[ + panels.OwnerPanel(), + ObjectsTablePanel('users.Group', filters={'owner_id': lambda ctx: ctx['object'].pk}), + ObjectsTablePanel('users.User', filters={'owner_id': lambda ctx: ctx['object'].pk}), + ], + right_panels=[ + RelatedObjectsPanel(), + ], + ) def get_extra_context(self, request, instance): return {