Sfoglia il codice sorgente

Closes #9708: Render user API tokens in a table

jeremystretch 3 anni fa
parent
commit
123e758c6d

+ 3 - 3
netbox/templates/inc/profile_button.html

@@ -19,17 +19,17 @@
         {% endif %}
         {% endif %}
       </li>
       </li>
       <li>
       <li>
-        <a class="dropdown-item" href="{% url 'user:profile' %}">
+        <a class="dropdown-item" href="{% url 'users:profile' %}">
           <i class="mdi mdi-account"></i> Profile
           <i class="mdi mdi-account"></i> Profile
         </a>
         </a>
       </li>
       </li>
       <li>
       <li>
-        <a class="dropdown-item" href="{% url 'user:preferences' %}">
+        <a class="dropdown-item" href="{% url 'users:preferences' %}">
           <i class="mdi mdi-wrench"></i> Preferences
           <i class="mdi mdi-wrench"></i> Preferences
         </a>
         </a>
       </li>
       </li>
       <li>
       <li>
-        <a class="dropdown-item" href="{% url 'user:token_list' %}">
+        <a class="dropdown-item" href="{% url 'users:token_list' %}">
           <i class="mdi mdi-key"></i> API Tokens
           <i class="mdi mdi-key"></i> API Tokens
         </a>
         </a>
       </li>
       </li>

+ 17 - 70
netbox/templates/users/api_tokens.html

@@ -1,78 +1,25 @@
 {% extends 'users/base.html' %}
 {% extends 'users/base.html' %}
 {% load helpers %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}API Tokens{% endblock %}
 {% block title %}API Tokens{% endblock %}
 
 
 {% block content %}
 {% block content %}
-    <div class="row">
-        <div class="col col-md-10 offset-md-1">
-            {% for token in tokens %}
-                <div class="card{% if token.is_expired %} bg-danger{% endif %}">
-                    <div class="card-header">
-                        <div class="float-end noprint">
-                            <a class="m-1 btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
-                            <a href="{% url 'user:token_edit' pk=token.pk %}" class="m-1 btn btn-sm btn-warning">Edit</a>
-                            <a href="{% url 'user:token_delete' pk=token.pk %}" class="m-1 btn btn-sm btn-danger">Delete</a>
-                        </div>
-                        <i class="mdi mdi-key"></i>
-                        <samp><span id="token_{{ token.pk }}">{{ token.key }}</span></samp>
-                        {% if token.is_expired %}
-                            <span class="badge bg-danger">Expired</span>
-                        {% endif %}
-                    </div>
-                    <div class="card-body">
-                        <div class="row">
-                            <div class="col col-md-3">
-                                <small class="text-muted">Created</small><br />
-                                {{ token.created|annotated_date }}
-                            </div>
-                            <div class="col col-md-3">
-                                <small class="text-muted">Expires</small><br />
-                                {% if token.expires %}
-                                    {{ token.expires|annotated_date }}
-                                {% else %}
-                                    <span>Never</span>
-                                {% endif %}
-                            </div>
-                            <div class="col col-md-3">
-                                <small class="text-muted">Last Used</small><br />
-                                {% if token.last_used %}
-                                    {{ token.last_used|annotated_date }}
-                                {% else %}
-                                    <span>Never</span>
-                                {% endif %}
-                            </div>
-                            <div class="col col-md-3">
-                                <small class="text-muted">Create/Edit/Delete Operations</small><br />
-                                {% if token.write_enabled %}
-                                    <span class="badge bg-success">Enabled</span>
-                                {% else %}
-                                    <span class="badge bg-danger">Disabled</span>
-                                {% endif %}
-                            </div>
-                            <div class="col col-md-3">
-                                <small class="text-muted">Allowed Source IPs</small><br />
-                                {% if token.allowed_ips %}
-                                    {{ token.allowed_ips|join:', ' }}
-                                {% else %}
-                                    <span>Any</span>
-                                {% endif %}
-                            </div>                        </div>
-                        {% if token.description %}
-                            <br /><span>{{ token.description }}</span>
-                        {% endif %}
-                    </div>
-                </div>
-            {% empty %}
-              <h6><i class="mdi mdi-information"></i> You do not have any API tokens.</h6>
-              <p>Tokens are used to authenticate REST and GraphQL API requests.</p>
-            {% endfor %}
-            <div class="text-end">
-              <a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3">
-                <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
-                Add a Token
-              </a>
-            </div>
-        </div>
+<div class="row">
+	<div class="col col-md-12 text-end">
+    <a href="{% url 'users:token_add' %}" class="btn btn-sm btn-primary my-3">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a Token
+    </a>
+  </div>
+</div>
+<div class="row mb-3">
+	<div class="col col-md-12">
+    <div class="card">
+      <div class="card-body table-responsive">
+        {% render_table table 'inc/table.html' %}
+        {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+      </div>
     </div>
     </div>
+  </div>
+</div>
 {% endblock %}
 {% endblock %}

+ 4 - 4
netbox/templates/users/base.html

@@ -3,18 +3,18 @@
 {% block tabs %}
 {% block tabs %}
   <ul class="nav nav-tabs px-3">
   <ul class="nav nav-tabs px-3">
     <li role="presentation" class="nav-item">
     <li role="presentation" class="nav-item">
-      <a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'user:profile' %}">Profile</a>
+      <a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'users:profile' %}">Profile</a>
     </li>
     </li>
     <li role="presentation" class="nav-item">
     <li role="presentation" class="nav-item">
-      <a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'user:preferences' %}">Preferences</a>
+      <a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'users:preferences' %}">Preferences</a>
     </li>
     </li>
     {% if not request.user.ldap_username %}
     {% if not request.user.ldap_username %}
       <li role="presentation" class="nav-item">
       <li role="presentation" class="nav-item">
-        <a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'user:change_password' %}">Password</a>
+        <a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'users:change_password' %}">Password</a>
       </li>
       </li>
     {% endif %}
     {% endif %}
     <li role="presentation" class="nav-item">
     <li role="presentation" class="nav-item">
-      <a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'user:token_list' %}">API Tokens</a>
+      <a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:token_list' %}">API Tokens</a>
     </li>
     </li>
   </ul>
   </ul>
 {% endblock %}
 {% endblock %}

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

@@ -13,7 +13,7 @@
             {% render_field form.new_password2 %}
             {% render_field form.new_password2 %}
         </div>
         </div>
         <div class="text-end">
         <div class="text-end">
-            <a href="{% url 'user:profile' %}" class="btn btn-outline-danger">Cancel</a>
+            <a href="{% url 'users:profile' %}" class="btn btn-outline-danger">Cancel</a>
             <button type="submit" name="_update" class="btn btn-primary">Save</button>
             <button type="submit" name="_update" class="btn btn-primary">Save</button>
         </div>
         </div>
     </form>
     </form>

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

@@ -79,7 +79,7 @@
     </div>
     </div>
 
 
     <div class="text-end my-3">
     <div class="text-end my-3">
-      <a class="btn btn-outline-secondary" href="{% url 'user:preferences' %}">Cancel</a>
+      <a class="btn btn-outline-secondary" href="{% url 'users:preferences' %}">Cancel</a>
       <button type="submit" name="_update" class="btn btn-primary">Save </button>
       <button type="submit" name="_update" class="btn btn-primary">Save </button>
     </div>
     </div>
   </form>
   </form>

+ 42 - 0
netbox/users/tables.py

@@ -0,0 +1,42 @@
+from .models import Token
+from netbox.tables import NetBoxTable, columns
+
+__all__ = (
+    'TokenTable',
+)
+
+
+TOKEN = """<samp><span id="token_{{ record.pk }}">{{ value }}</span></samp>"""
+
+ALLOWED_IPS = """{{ value|join:", " }}"""
+
+COPY_BUTTON = """
+<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
+  <i class="mdi mdi-content-copy"></i>
+</a>
+"""
+
+
+class TokenTable(NetBoxTable):
+    key = columns.TemplateColumn(
+        template_code=TOKEN
+    )
+    write_enabled = columns.BooleanColumn(
+        verbose_name='Write'
+    )
+    created = columns.DateColumn()
+    expired = columns.DateColumn()
+    last_used = columns.DateTimeColumn()
+    allowed_ips = columns.TemplateColumn(
+        template_code=ALLOWED_IPS
+    )
+    actions = columns.ActionsColumn(
+        actions=('edit', 'delete'),
+        extra_buttons=COPY_BUTTON
+    )
+
+    class Meta(NetBoxTable.Meta):
+        model = Token
+        fields = (
+            'pk', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', 'description',
+        )

+ 1 - 1
netbox/users/urls.py

@@ -2,7 +2,7 @@ from django.urls import path
 
 
 from . import views
 from . import views
 
 
-app_name = 'user'
+app_name = 'users'
 urlpatterns = [
 urlpatterns = [
 
 
     path('profile/', views.ProfileView.as_view(), name='profile'),
     path('profile/', views.ProfileView.as_view(), name='profile'),

+ 14 - 10
netbox/users/views.py

@@ -21,6 +21,7 @@ from netbox.config import get_config
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
 from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
 from .models import Token
 from .models import Token
+from .tables import TokenTable
 
 
 
 
 #
 #
@@ -157,7 +158,7 @@ class UserConfigView(LoginRequiredMixin, View):
             form.save()
             form.save()
 
 
             messages.success(request, "Your preferences have been updated.")
             messages.success(request, "Your preferences have been updated.")
-            return redirect('user:preferences')
+            return redirect('users:preferences')
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
@@ -172,7 +173,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
         # LDAP users cannot change their password here
         # LDAP users cannot change their password here
         if getattr(request.user, 'ldap_username', None):
         if getattr(request.user, 'ldap_username', None):
             messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
             messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
-            return redirect('user:profile')
+            return redirect('users:profile')
 
 
         form = PasswordChangeForm(user=request.user)
         form = PasswordChangeForm(user=request.user)
 
 
@@ -187,7 +188,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
             form.save()
             form.save()
             update_session_auth_hash(request, form.user)
             update_session_auth_hash(request, form.user)
             messages.success(request, "Your password has been changed successfully.")
             messages.success(request, "Your password has been changed successfully.")
-            return redirect('user:profile')
+            return redirect('users:profile')
 
 
         return render(request, self.template_name, {
         return render(request, self.template_name, {
             'form': form,
             'form': form,
@@ -204,10 +205,13 @@ class TokenListView(LoginRequiredMixin, View):
     def get(self, request):
     def get(self, request):
 
 
         tokens = Token.objects.filter(user=request.user)
         tokens = Token.objects.filter(user=request.user)
+        table = TokenTable(tokens)
+        table.configure(request)
 
 
         return render(request, 'users/api_tokens.html', {
         return render(request, 'users/api_tokens.html', {
             'tokens': tokens,
             'tokens': tokens,
             'active_tab': 'api-tokens',
             'active_tab': 'api-tokens',
+            'table': table,
         })
         })
 
 
 
 
@@ -225,7 +229,7 @@ class TokenEditView(LoginRequiredMixin, View):
         return render(request, 'generic/object_edit.html', {
         return render(request, 'generic/object_edit.html', {
             'object': token,
             'object': token,
             'form': form,
             'form': form,
-            'return_url': reverse('user:token_list'),
+            'return_url': reverse('users:token_list'),
         })
         })
 
 
     def post(self, request, pk=None):
     def post(self, request, pk=None):
@@ -248,12 +252,12 @@ class TokenEditView(LoginRequiredMixin, View):
             if '_addanother' in request.POST:
             if '_addanother' in request.POST:
                 return redirect(request.path)
                 return redirect(request.path)
             else:
             else:
-                return redirect('user:token_list')
+                return redirect('users:token_list')
 
 
         return render(request, 'generic/object_edit.html', {
         return render(request, 'generic/object_edit.html', {
             'object': token,
             'object': token,
             'form': form,
             'form': form,
-            'return_url': reverse('user:token_list'),
+            'return_url': reverse('users:token_list'),
         })
         })
 
 
 
 
@@ -263,14 +267,14 @@ class TokenDeleteView(LoginRequiredMixin, View):
 
 
         token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
         token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
         initial_data = {
         initial_data = {
-            'return_url': reverse('user:token_list'),
+            'return_url': reverse('users:token_list'),
         }
         }
         form = ConfirmationForm(initial=initial_data)
         form = ConfirmationForm(initial=initial_data)
 
 
         return render(request, 'generic/object_delete.html', {
         return render(request, 'generic/object_delete.html', {
             'object': token,
             'object': token,
             'form': form,
             'form': form,
-            'return_url': reverse('user:token_list'),
+            'return_url': reverse('users:token_list'),
         })
         })
 
 
     def post(self, request, pk):
     def post(self, request, pk):
@@ -280,10 +284,10 @@ class TokenDeleteView(LoginRequiredMixin, View):
         if form.is_valid():
         if form.is_valid():
             token.delete()
             token.delete()
             messages.success(request, "Token deleted")
             messages.success(request, "Token deleted")
-            return redirect('user:token_list')
+            return redirect('users:token_list')
 
 
         return render(request, 'generic/object_delete.html', {
         return render(request, 'generic/object_delete.html', {
             'object': token,
             'object': token,
             'form': form,
             'form': form,
-            'return_url': reverse('user:token_list'),
+            'return_url': reverse('users:token_list'),
         })
         })