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">»</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