From 59400e95134702d2443f50f8c52efcc924d7ad8d Mon Sep 17 00:00:00 2001
From: Robin Tissot <tissotrobin@gmail.com>
Date: Wed, 22 Jul 2020 16:18:15 +0200
Subject: [PATCH 1/4] Profile page with api token and list of exported files.

---
 app/apps/api/urls.py                          |  4 +-
 app/apps/users/forms.py                       | 10 ++++-
 app/apps/users/urls.py                        |  3 +-
 app/apps/users/views.py                       | 39 ++++++++++++++++++-
 app/escriptorium/settings.py                  |  6 +++
 app/escriptorium/templates/base.html          |  2 +-
 app/escriptorium/templates/users/profile.html | 35 +++++++++++++++++
 7 files changed, 92 insertions(+), 7 deletions(-)
 create mode 100644 app/escriptorium/templates/users/profile.html

diff --git a/app/apps/api/urls.py b/app/apps/api/urls.py
index 8f1478a4..80ec98c6 100644
--- a/app/apps/api/urls.py
+++ b/app/apps/api/urls.py
@@ -1,5 +1,6 @@
 from django.urls import include, path
 from rest_framework_nested import routers
+from rest_framework.authtoken import views
 
 from api.views import (DocumentViewSet,
                        UserViewSet,
@@ -29,5 +30,6 @@ urlpatterns = [
     path('', include(router.urls)),
     path('', include(documents_router.urls)),
     path('', include(parts_router.urls)),
-    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
+    path('token-auth/', views.obtain_auth_token)
 ]
diff --git a/app/apps/users/forms.py b/app/apps/users/forms.py
index 1e7729e7..ebd31ff9 100644
--- a/app/apps/users/forms.py
+++ b/app/apps/users/forms.py
@@ -32,7 +32,7 @@ class InvitationAcceptForm(BootstrapFormMixin, UserCreationForm):
     """
     This is a registration form since a user is created.
     """
-    
+
     class Meta(UserCreationForm.Meta):
         model = User
         fields = ('email',
@@ -41,8 +41,14 @@ class InvitationAcceptForm(BootstrapFormMixin, UserCreationForm):
                   'last_name',
                   'password1',
                   'password2')
-    
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.fields['username'].help_text = None
         self.fields['password1'].help_text = None
+
+
+class ProfileForm(BootstrapFormMixin, forms.ModelForm):
+    class Meta:
+        model = User
+        fields = ('email', 'first_name', 'last_name')
diff --git a/app/apps/users/urls.py b/app/apps/users/urls.py
index d1b6dd8e..7259741b 100644
--- a/app/apps/users/urls.py
+++ b/app/apps/users/urls.py
@@ -1,10 +1,11 @@
 from django.urls import path, include
 
-from users.views import SendInvitation, AcceptInvitation
+from users.views import SendInvitation, AcceptInvitation, Profile
 from django.contrib.auth.decorators import permission_required
 
 urlpatterns = [
     path('', include('django.contrib.auth.urls')),
+    path('profile/', Profile.as_view(), name='user_profile'),
     path('invite/',
          permission_required('users.can_invite', raise_exception=True)(SendInvitation.as_view()),
          name='send-invitation'),
diff --git a/app/apps/users/views.py b/app/apps/users/views.py
index d81006ed..77d2a817 100644
--- a/app/apps/users/views.py
+++ b/app/apps/users/views.py
@@ -1,13 +1,19 @@
+from os import listdir, stat
+from os.path import isfile, join, relpath
+
+from django.conf import settings
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.messages.views import SuccessMessageMixin
+from django.core.paginator import Paginator
 from django.http import Http404
 from django.utils.functional import cached_property
 from django.utils.translation import gettext as _
-from django.views.generic.edit import CreateView
+from django.views.generic.edit import CreateView, UpdateView
 
+from rest_framework.authtoken.models import Token
 
 from users.models import User, Invitation
-from users.forms import InvitationForm, InvitationAcceptForm
+from users.forms import InvitationForm, InvitationAcceptForm, ProfileForm
 
 
 class SendInvitation(LoginRequiredMixin, SuccessMessageMixin, CreateView):
@@ -68,3 +74,32 @@ class AcceptInvitation(CreateView):
         return response
 
 # request invitation
+
+
+class Profile(SuccessMessageMixin, UpdateView):
+    model = User
+    form_class = ProfileForm
+    success_url = '/profile/'
+    success_message = _('Profile successfully saved.')
+    template_name = 'users/profile.html'
+
+    def get_object(self):
+        return self.request.user
+
+    def get_context_data(self):
+        context = super().get_context_data()
+        context['api_auth_token'], created = Token.objects.get_or_create(user=self.object)
+
+        # files directory
+        upath = self.object.get_document_store_path() + '/'
+        files = listdir(upath)
+        files.sort(key=lambda x: stat(join(upath, x)).st_mtime)
+        files.reverse()
+        files = [(relpath(join(upath, f), settings.MEDIA_ROOT), f)
+                 for f in files
+                 if isfile(join(upath, f))]
+        paginator = Paginator(files, 25)  # Show 25 files per page.
+        page_number = self.request.GET.get('page')
+        context['is_paginated'] = True
+        context['page_obj'] = paginator.get_page(page_number)
+        return context
diff --git a/app/escriptorium/settings.py b/app/escriptorium/settings.py
index d3bd5962..116ea6d7 100644
--- a/app/escriptorium/settings.py
+++ b/app/escriptorium/settings.py
@@ -57,6 +57,7 @@ INSTALLED_APPS = [
     'easy_thumbnails.optimize',
     'channels',
     'rest_framework',
+    'rest_framework.authtoken',
     'compressor',
 
     'bootstrap',
@@ -314,6 +315,11 @@ REST_FRAMEWORK = {
         # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
         'rest_framework.permissions.IsAuthenticated'
     ],
+    'DEFAULT_AUTHENTICATION_CLASSES': [
+        # 'rest_framework.authentication.BasicAuthentication',  # Only for testing
+        'rest_framework.authentication.TokenAuthentication',
+        'rest_framework.authentication.SessionAuthentication',
+    ],
     'DEFAULT_PAGINATION_CLASS': 'core.pagination.CustomPagination',
     'PAGE_SIZE': 10,
 }
diff --git a/app/escriptorium/templates/base.html b/app/escriptorium/templates/base.html
index 30b4b82b..3167a746 100644
--- a/app/escriptorium/templates/base.html
+++ b/app/escriptorium/templates/base.html
@@ -74,7 +74,7 @@
               </a>
               <div class="dropdown-menu  dropdown-menu-right" aria-labelledby="navbarDropdown">
               <a class="dropdown-item" href="{% url 'password_change' %}">{% trans "Change password" %}</a>
-              <a class="dropdown-item" href="#">{% trans "Profile" %}</a>
+              <a class="dropdown-item" href="{% url 'user_profile' %}">{% trans "Profile" %}</a>
               <div class="dropdown-divider"></div>
               {% if perms.users.can_invite %}<a class="dropdown-item" href="{% url 'send-invitation' %}">Invite</a>{% endif %}
               <a class="dropdown-item" href="{% url 'logout' %}">{% trans 'Logout' %}</a>
diff --git a/app/escriptorium/templates/users/profile.html b/app/escriptorium/templates/users/profile.html
new file mode 100644
index 00000000..68b22e6c
--- /dev/null
+++ b/app/escriptorium/templates/users/profile.html
@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+{% load i18n bootstrap static %}
+
+{% block body %}
+<div class="container">
+<div class="row">
+    <div class="col-md-10 col-md-offset-10">
+
+        <form method="post">
+            {% csrf_token %}
+            <fieldset>
+                {% render_field form.email %}
+                {% render_field form.first_name %}
+                {% render_field form.last_name %}
+
+                <input type="submit" value="{% trans 'Save' %}" class="btn btn-lg btn-success btn-block">
+            </fieldset>
+        </form>
+
+        <p class="form-text alert alert-secondary mt-5">
+            {% trans "API Authentication Token:" %} {{ api_auth_token.key }}
+            <br/><span class="text-muted"><small>{% trans "example: "%} $ curl {{request.scheme}}://{{request.get_host}}/api/documents/ -H 'Authorization: Token {{ api_auth_token.key }}'</small></span>
+        </p>
+
+        <div class="form-text jumbotron mt-3">
+            <h2>{% trans "All my files" %}</h2>
+            {% for fpath, fname in page_obj %}
+            <br><a href="{{request.scheme}}://{{request.get_host}}{% get_media_prefix %}{{fpath}}"><i class="fas fa-download"></i></a> {{ fname }}
+            {% endfor %}
+            {% include "includes/pagination.html" %}
+        </div>
+    </div>
+</div>
+</div>
+{% endblock %}
-- 
GitLab


From 0cbc499a859634931ce34b8b3fa8f00e4ac76b5e Mon Sep 17 00:00:00 2001
From: Robin Tissot <tissotrobin@gmail.com>
Date: Wed, 22 Jul 2020 16:26:44 +0200
Subject: [PATCH 2/4] fix files pagination when there is no files.

---
 app/apps/users/views.py                       | 2 +-
 app/escriptorium/templates/users/profile.html | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/app/apps/users/views.py b/app/apps/users/views.py
index 77d2a817..c624e15f 100644
--- a/app/apps/users/views.py
+++ b/app/apps/users/views.py
@@ -100,6 +100,6 @@ class Profile(SuccessMessageMixin, UpdateView):
                  if isfile(join(upath, f))]
         paginator = Paginator(files, 25)  # Show 25 files per page.
         page_number = self.request.GET.get('page')
-        context['is_paginated'] = True
+        context['is_paginated'] = paginator.count != 0
         context['page_obj'] = paginator.get_page(page_number)
         return context
diff --git a/app/escriptorium/templates/users/profile.html b/app/escriptorium/templates/users/profile.html
index 68b22e6c..7a03a63b 100644
--- a/app/escriptorium/templates/users/profile.html
+++ b/app/escriptorium/templates/users/profile.html
@@ -26,7 +26,10 @@
             <h2>{% trans "All my files" %}</h2>
             {% for fpath, fname in page_obj %}
             <br><a href="{{request.scheme}}://{{request.get_host}}{% get_media_prefix %}{{fpath}}"><i class="fas fa-download"></i></a> {{ fname }}
+            {% empty %}
+            {% trans "You don't have any saved files yet." %}
             {% endfor %}
+
             {% include "includes/pagination.html" %}
         </div>
     </div>
-- 
GitLab


From 19266c9c1999b306178a6caeb76d1112c923cf4a Mon Sep 17 00:00:00 2001
From: Robin Tissot <tissotrobin@gmail.com>
Date: Thu, 23 Jul 2020 11:54:39 +0200
Subject: [PATCH 3/4] Better looking profile.

---
 .../templates/includes/pagination.html        |  4 +-
 app/escriptorium/templates/users/profile.html | 91 +++++++++++++------
 2 files changed, 64 insertions(+), 31 deletions(-)

diff --git a/app/escriptorium/templates/includes/pagination.html b/app/escriptorium/templates/includes/pagination.html
index cfe00e4e..8c38b17c 100644
--- a/app/escriptorium/templates/includes/pagination.html
+++ b/app/escriptorium/templates/includes/pagination.html
@@ -12,7 +12,7 @@
       <a class="page-link" title="{% trans 'First page' %}" href="?page=1">1</a>
     </li>
     {% endif %}
-    
+
     <li class="page-item active">
       <a class="page-link" href="#">{{page_obj.number}} <span class="sr-only">(current)</span></a>
     </li>
@@ -22,7 +22,7 @@
       <a class="page-link" title="{% trans 'Last page' %}" href="?page={{page_obj.paginator.num_pages}}">{{page_obj.paginator.num_pages}}</a>
     </li>
     {% endif %}
-    
+
     <li class="page-item {% if not page_obj.has_next %}disabled{% endif %}">
       <a class="page-link" href="{% if page_obj.has_next %}?page={{page_obj.next_page_number}}{% else %}#{% endif %}"><span aria-hidden="true">&raquo;</span> <span class="sr-only">{% trans "Next" %}</span></a>
     </li>
diff --git a/app/escriptorium/templates/users/profile.html b/app/escriptorium/templates/users/profile.html
index 7a03a63b..812654d3 100644
--- a/app/escriptorium/templates/users/profile.html
+++ b/app/escriptorium/templates/users/profile.html
@@ -3,36 +3,69 @@
 
 {% block body %}
 <div class="container">
-<div class="row">
-    <div class="col-md-10 col-md-offset-10">
-
-        <form method="post">
-            {% csrf_token %}
-            <fieldset>
-                {% render_field form.email %}
-                {% render_field form.first_name %}
-                {% render_field form.last_name %}
-
-                <input type="submit" value="{% trans 'Save' %}" class="btn btn-lg btn-success btn-block">
-            </fieldset>
-        </form>
-
-        <p class="form-text alert alert-secondary mt-5">
-            {% trans "API Authentication Token:" %} {{ api_auth_token.key }}
-            <br/><span class="text-muted"><small>{% trans "example: "%} $ curl {{request.scheme}}://{{request.get_host}}/api/documents/ -H 'Authorization: Token {{ api_auth_token.key }}'</small></span>
-        </p>
-
-        <div class="form-text jumbotron mt-3">
-            <h2>{% trans "All my files" %}</h2>
-            {% for fpath, fname in page_obj %}
-            <br><a href="{{request.scheme}}://{{request.get_host}}{% get_media_prefix %}{{fpath}}"><i class="fas fa-download"></i></a> {{ fname }}
-            {% empty %}
-            {% trans "You don't have any saved files yet." %}
-            {% endfor %}
-
-            {% include "includes/pagination.html" %}
+    <div class="row">
+        <div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
+            <a class="nav-link active" id="nav-infos-tab" data-toggle="pill" href="#infos-tab" role="tab">{% trans "Informations" %}</a>
+            <a class="nav-link" id="nav-key-tab" data-toggle="pill" href="#key-tab" role="tab">{% trans "Api key" %}</a>
+            <a class="nav-link" id="nav-files-tab" data-toggle="pill" href="#files-tab" role="tab">{% trans "Files" %}</a>
+        </div>
+
+        <div class="col-md-8 tab-content" id="v-pills-tabContent">
+            <div class="tab-pane fade show active" id="infos-tab" role="tabpanel" aria-labelledby="v-pills-home-tab">
+                <form method="post">
+                    {% csrf_token %}
+                    <fieldset>
+                        {% render_field form.email %}
+                        {% render_field form.first_name %}
+                        {% render_field form.last_name %}
+
+                        <input type="submit" value="{% trans 'Save' %}" class="btn btn-lg btn-success btn-block">
+                    </fieldset>
+                </form>
+            </div>
+
+            <div class="tab-pane fade show" id="key-tab" role="tabpanel" aria-labelledby="v-pills-home-tab">
+                {% trans "API Authentication Token:" %} {{ api_auth_token.key }}
+                <br/><span class="text-muted"><small>{% trans "example: "%} $ curl {{request.scheme}}://{{request.get_host}}/api/documents/ -H 'Authorization: Token {{ api_auth_token.key }}'</small></span>
+            </div>
+
+            <div class="tab-pane fade show" id="files-tab" role="tabpanel" aria-labelledby="v-pills-home-tab">
+                {% for fpath, fname in page_obj %}
+                <br><a href="{{request.scheme}}://{{request.get_host}}{% get_media_prefix %}{{fpath}}"><i class="fas fa-download"></i></a> {{ fname }}
+                {% empty %}
+                {% trans "You don't have any saved files yet." %}
+                {% endfor %}
+
+                {% include "includes/pagination.html" %}
+            </div>
         </div>
     </div>
 </div>
-</div>
+{% endblock %}
+
+{% block scripts %}
+{{ block.super }}
+<script>
+ $(document).ready(function() {
+     function updatePagination(hash) {
+         $('ul.pagination a').each(function(i, a) {
+             let href = $(a).attr("href").split('#')[0];
+             $(a).attr("href", href+location.hash);
+         });
+     }
+
+     if (location.hash) {
+         let tab = location.hash.split('#')[1];
+         $('#nav-' + tab).tab("show");
+         updatePagination(location.hash);
+     }
+     $('a[data-toggle="pill"]').on("click", function() {
+         let url = location.href;
+         let hash = $(this).attr("href");
+         let newUrl = url.split("#")[0] + hash;
+         history.replaceState(null, null, newUrl);
+         updatePagination(hash);
+     });
+ }, false);
+</script>
 {% endblock %}
-- 
GitLab


From 81e27944a28353b9a8c3a09047065851709fa97b Mon Sep 17 00:00:00 2001
From: Robin Tissot <tissotrobin@gmail.com>
Date: Thu, 23 Jul 2020 13:41:29 +0200
Subject: [PATCH 4/4] Add a copy to clipboard button for the api key.

---
 app/escriptorium/templates/users/profile.html | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/app/escriptorium/templates/users/profile.html b/app/escriptorium/templates/users/profile.html
index 812654d3..8c6e7bc9 100644
--- a/app/escriptorium/templates/users/profile.html
+++ b/app/escriptorium/templates/users/profile.html
@@ -26,6 +26,7 @@
 
             <div class="tab-pane fade show" id="key-tab" role="tabpanel" aria-labelledby="v-pills-home-tab">
                 {% trans "API Authentication Token:" %} {{ api_auth_token.key }}
+                <button class="btn btn-secondary btn-sm fas fa-clipboard float-right" id="api-key-clipboard" title="{% trans "Copy to clipboard." %}" data-key="{{ api_auth_token.key }}"></button>
                 <br/><span class="text-muted"><small>{% trans "example: "%} $ curl {{request.scheme}}://{{request.get_host}}/api/documents/ -H 'Authorization: Token {{ api_auth_token.key }}'</small></span>
             </div>
 
@@ -47,6 +48,10 @@
 {{ block.super }}
 <script>
  $(document).ready(function() {
+     $('#api-key-clipboard').click(function() {
+         navigator.clipboard.writeText($(this).data('key'))
+     });
+
      function updatePagination(hash) {
          $('ul.pagination a').each(function(i, a) {
              let href = $(a).attr("href").split('#')[0];
-- 
GitLab