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

Closes #9708: Render user API tokens in a table

jeremystretch 3 лет назад
Родитель
Сommit
123e758c6d

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

@@ -19,17 +19,17 @@
         {% endif %}
       </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
         </a>
       </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
         </a>
       </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
         </a>
       </li>

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

@@ -1,78 +1,25 @@
 {% extends 'users/base.html' %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 
 {% block title %}API Tokens{% endblock %}
 
 {% 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>
 {% endblock %}

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

@@ -3,18 +3,18 @@
 {% block tabs %}
   <ul class="nav nav-tabs px-3">
     <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 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>
     {% if not request.user.ldap_username %}
       <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>
     {% endif %}
     <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>
   </ul>
 {% endblock %}

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

@@ -13,7 +13,7 @@
             {% render_field form.new_password2 %}
         </div>
         <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>
         </div>
     </form>

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

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