From 95c49fd364421dd86d48b07191f2fec2db21bb92 Mon Sep 17 00:00:00 2001 From: Roman P Date: Fri, 5 Jul 2019 19:12:14 +0400 Subject: [PATCH] Fix broken password change feature Closes #300 Actually I had to refactor the whole profile page's layout and split it into 3 pages --- backend/backend/settings/base.py | 1 + backend/profile_page/forms.py | 9 +- .../migrations/0003_auto_20190705_1446.py | 19 +++ .../migrations/0004_merge_20190708_0744.py | 14 ++ backend/profile_page/models.py | 2 +- backend/profile_page/templates/profile.html | 143 ------------------ .../templates/profile_account.html | 61 ++++++++ .../templates/profile_password.html | 61 ++++++++ .../profile_page/templates/profile_token.html | 59 ++++++++ backend/profile_page/tests.py | 39 ++++- backend/profile_page/urls.py | 9 +- backend/profile_page/views.py | 47 +++--- requirements.txt | 1 + 13 files changed, 291 insertions(+), 174 deletions(-) create mode 100644 backend/profile_page/migrations/0003_auto_20190705_1446.py create mode 100644 backend/profile_page/migrations/0004_merge_20190708_0744.py delete mode 100644 backend/profile_page/templates/profile.html create mode 100644 backend/profile_page/templates/profile_account.html create mode 100644 backend/profile_page/templates/profile_password.html create mode 100644 backend/profile_page/templates/profile_token.html diff --git a/backend/backend/settings/base.py b/backend/backend/settings/base.py index b064915dc..277dea980 100644 --- a/backend/backend/settings/base.py +++ b/backend/backend/settings/base.py @@ -66,6 +66,7 @@ def check_ip_range(ipr): 'tagulous', 'device_registry.apps.DeviceRegistryConfig', 'profile_page.apps.ProfilePageConfig', + 'bootstrap4' ] MIDDLEWARE = [ diff --git a/backend/profile_page/forms.py b/backend/profile_page/forms.py index c7aaee30b..b28c4d1b5 100644 --- a/backend/profile_page/forms.py +++ b/backend/profile_page/forms.py @@ -2,7 +2,8 @@ class ProfileForm(forms.Form): - email = forms.CharField() - first_name = forms.CharField(required=False) - last_name = forms.CharField(required=False) - company = forms.CharField(required=False) + username = forms.CharField(disabled=True) + email = forms.EmailField() + first_name = forms.CharField(max_length=30, required=False) + last_name = forms.CharField(max_length=150, required=False) + company = forms.CharField(max_length=128, required=False) diff --git a/backend/profile_page/migrations/0003_auto_20190705_1446.py b/backend/profile_page/migrations/0003_auto_20190705_1446.py new file mode 100644 index 000000000..ec3d1efe7 --- /dev/null +++ b/backend/profile_page/migrations/0003_auto_20190705_1446.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.10 on 2019-07-05 14:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profile_page', '0002_auto_20190605_1012'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='company_name', + field=models.CharField(blank=True, default='', max_length=128), + preserve_default=False, + ), + ] diff --git a/backend/profile_page/migrations/0004_merge_20190708_0744.py b/backend/profile_page/migrations/0004_merge_20190708_0744.py new file mode 100644 index 000000000..439e95e11 --- /dev/null +++ b/backend/profile_page/migrations/0004_merge_20190708_0744.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.10 on 2019-07-08 07:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('profile_page', '0003_auto_20190705_1446'), + ('profile_page', '0003_profile_last_active'), + ] + + operations = [ + ] diff --git a/backend/profile_page/models.py b/backend/profile_page/models.py index 20431752c..a30eb212e 100644 --- a/backend/profile_page/models.py +++ b/backend/profile_page/models.py @@ -11,5 +11,5 @@ def user_save_lower(sender, instance, *args, **kwargs): class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - company_name = models.CharField(blank=True, null=True, max_length=128) last_active = models.DateField(null=True, blank=True) + company_name = models.CharField(blank=True, max_length=128) diff --git a/backend/profile_page/templates/profile.html b/backend/profile_page/templates/profile.html deleted file mode 100644 index 501265f7c..000000000 --- a/backend/profile_page/templates/profile.html +++ /dev/null @@ -1,143 +0,0 @@ -{% extends "admin_base.html" %} - -{% block title %}WoTT - Dashboard{% endblock title %} - -{% block dashboard_title %} -

Dashboard

-{% endblock dashboard_title %} - -{% block admin_content %} -
- -

Settings

- -
-
- -
-
-
Profile Settings
-
- - -
-
- -
-
-
- -
-
-
Public info
-
-
-
- {% csrf_token %} -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
-
-
-
-
-
-
Password
-
-
-
- {% csrf_token %} -
- - -
-
- - -
-
- - -
- -
-
-
-
-
-
-
-
API token
-
-
- {% if user.auth_token %} -
{{ user.auth_token }}
- Revoke token - {% else %} - Generate token - {% endif %} -
-
-
-
-
-
-
-{% endblock %} - -{% block scripts %} - -{% endblock %} \ No newline at end of file diff --git a/backend/profile_page/templates/profile_account.html b/backend/profile_page/templates/profile_account.html new file mode 100644 index 000000000..cf8fca1fb --- /dev/null +++ b/backend/profile_page/templates/profile_account.html @@ -0,0 +1,61 @@ +{% extends "admin_base.html" %} + +{% load bootstrap4 %} + +{% block title %}WoTT - Dashboard{% endblock title %} + +{% block dashboard_title %} +

Dashboard

+{% endblock dashboard_title %} + +{% block admin_content %} +
+ +

Settings

+ +
+
+ +
+
+
Profile Settings
+
+ + +
+
+ +
+
+
+ +
+
+
Public info
+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/profile_page/templates/profile_password.html b/backend/profile_page/templates/profile_password.html new file mode 100644 index 000000000..5c8d3e010 --- /dev/null +++ b/backend/profile_page/templates/profile_password.html @@ -0,0 +1,61 @@ +{% extends "admin_base.html" %} + +{% load bootstrap4 %} + +{% block title %}WoTT - Dashboard{% endblock title %} + +{% block dashboard_title %} +

Dashboard

+{% endblock dashboard_title %} + +{% block admin_content %} +
+ +

Settings

+ +
+
+ +
+
+
Profile Settings
+
+ + +
+
+ +
+
+
+ +
+
+
Password
+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/profile_page/templates/profile_token.html b/backend/profile_page/templates/profile_token.html new file mode 100644 index 000000000..efda29c7e --- /dev/null +++ b/backend/profile_page/templates/profile_token.html @@ -0,0 +1,59 @@ +{% extends "admin_base.html" %} + +{% load bootstrap4 %} + +{% block title %}WoTT - Dashboard{% endblock title %} + +{% block dashboard_title %} +

Dashboard

+{% endblock dashboard_title %} + +{% block admin_content %} +
+ +

Settings

+ +
+
+ +
+
+
Profile Settings
+
+ + +
+
+ +
+
+
+
+
+
API token
+
+
+ {% if user.auth_token %} +
{{ user.auth_token }}
+ Revoke token + {% else %} + Generate token + {% endif %} +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/backend/profile_page/tests.py b/backend/profile_page/tests.py index 986270766..2cf5549b3 100644 --- a/backend/profile_page/tests.py +++ b/backend/profile_page/tests.py @@ -1,29 +1,27 @@ -from django.test import TestCase, RequestFactory +from django.test import TestCase from django.contrib.auth.models import User from django.urls import reverse from django.utils import timezone -class ProfileViewTest(TestCase): +class ProfileViewsTest(TestCase): def setUp(self): self.user0 = User.objects.create_user('test') self.user0.set_password('123') self.user0.save() + self.client.login(username='test', password='123') def test_get(self): - self.client.login(username='test', password='123') response = self.client.get(reverse('profile')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Profile Settings') self.assertEqual(self.user0.profile.last_active, timezone.localdate()) - def test_comment(self): - self.client.login(username='test', password='123') + def test_post(self): form_data = {'email': 'user@gmail.com', 'first_name': 'John', 'last_name': 'Doe', 'company': 'Acme Corporation'} - # Submit form data. response = self.client.post(reverse('profile'), form_data) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 302) # Load page for checking its content. response = self.client.get(reverse('profile')) self.assertEqual(response.status_code, 200) @@ -31,3 +29,30 @@ def test_comment(self): self.assertContains(response, 'John') self.assertContains(response, 'Doe') self.assertContains(response, 'Acme Corporation') + + def test_password_change_fail(self): + form_data = {'old_password': '123', 'new_password1': '321', 'new_password2': '321'} + response = self.client.post(reverse('profile_password'), form_data) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'This password is entirely numeric') + + def test_password_change_success(self): + form_data = {'old_password': '123', 'new_password1': 'Hy321_Uh9Gfde', 'new_password2': 'Hy321_Uh9Gfde'} + response = self.client.post(reverse('profile_password'), form_data) + self.assertEqual(response.status_code, 302) + + def test_token_page(self): + response = self.client.get(reverse('profile_token')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Generate token') + response = self.client.get(reverse('generate_api_token')) + self.assertEqual(response.status_code, 302) + response = self.client.get(reverse('profile_token')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.user0.auth_token) + self.assertContains(response, 'Revoke token') + response = self.client.get(reverse('revoke_api_token')) + self.assertEqual(response.status_code, 302) + response = self.client.get(reverse('profile_token')) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Generate token') diff --git a/backend/profile_page/urls.py b/backend/profile_page/urls.py index 22e0582bf..222b44914 100644 --- a/backend/profile_page/urls.py +++ b/backend/profile_page/urls.py @@ -1,9 +1,14 @@ from django.urls import path +from django.contrib.auth.views import PasswordChangeView +from django.urls import reverse_lazy -from .views import profile_view, GenerateAPITokenView, RevokeAPITokenView +from .views import GenerateAPITokenView, RevokeAPITokenView, ProfileAccountView, ProfileAPITokenView urlpatterns = [ - path('profile/', profile_view, name='profile'), + path('profile/', ProfileAccountView.as_view(), name='profile'), + path('profile/password/', PasswordChangeView.as_view( + success_url=reverse_lazy('profile_password'), template_name='profile_password.html'), name='profile_password'), + path('profile/token/', ProfileAPITokenView.as_view(), name='profile_token'), path('generate-api-token/', GenerateAPITokenView.as_view(), name='generate_api_token'), path('revoke-api-token/', RevokeAPITokenView.as_view(), name='revoke_api_token') ] diff --git a/backend/profile_page/views.py b/backend/profile_page/views.py index 109ed5f8e..9a492b2b7 100644 --- a/backend/profile_page/views.py +++ b/backend/profile_page/views.py @@ -1,11 +1,10 @@ from django.shortcuts import render -from django.contrib.auth.decorators import login_required from django.contrib.auth.views import LogoutView as DjangoLogoutView from django.contrib.auth import logout from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.contrib import messages -from django.views.generic import View +from django.views.generic import View, TemplateView from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.urls import reverse @@ -16,21 +15,35 @@ from .models import Profile -@login_required -def profile_view(request): - user = request.user - profile, _ = Profile.objects.get_or_create(user=user) - if request.method == 'POST': - form = ProfileForm(request.POST) +class ProfileAccountView(LoginRequiredMixin, View): + def dispatch(self, request, *args, **kwargs): + self.user = request.user + self.profile, _ = Profile.objects.get_or_create(user=self.user) + self.initial_form_data = {'username': self.user.username, 'email': self.user.email, + 'first_name': self.user.first_name, 'last_name': self.user.last_name, + 'company': self.profile.company_name} + return super().dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + form = ProfileForm(initial=self.initial_form_data) + return render(request, 'profile_account.html', {'form': form}) + + def post(self, request, *args, **kwargs): + form = ProfileForm(request.POST, initial=self.initial_form_data) if form.is_valid(): - user.email = form.cleaned_data['email'] - user.first_name = form.cleaned_data['first_name'] - user.last_name = form.cleaned_data['last_name'] - profile.company_name = form.cleaned_data['company'] - profile.save() - user.save() - return render(request, 'profile.html') + self.user.email = form.cleaned_data['email'] + self.user.first_name = form.cleaned_data['first_name'] + self.user.last_name = form.cleaned_data['last_name'] + self.profile.company_name = form.cleaned_data['company'] + self.user.save(update_fields=['email', 'first_name', 'last_name']) + self.profile.save(update_fields=['company_name']) + return HttpResponseRedirect(reverse('profile')) + return render(request, 'profile_account.html', {'form': form}) + + +class ProfileAPITokenView(LoginRequiredMixin, TemplateView): + template_name = 'profile_token.html' class LogoutView(DjangoLogoutView): @@ -54,11 +67,11 @@ class GenerateAPITokenView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): if not hasattr(request.user, 'auth_token'): Token.objects.create(user=request.user) - return HttpResponseRedirect(reverse('profile') + '#token') + return HttpResponseRedirect(reverse('profile_token')) class RevokeAPITokenView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): if hasattr(request.user, 'auth_token'): Token.objects.filter(user=request.user).delete() - return HttpResponseRedirect(reverse('profile') + '#token') + return HttpResponseRedirect(reverse('profile_token')) diff --git a/requirements.txt b/requirements.txt index d138931b2..523787f27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ cryptography==2.4.2 django-extensions==2.1.6 django-json-widget==0.2.0 django-registration-redux==2.6 +django-bootstrap4==0.0.8 djangorestframework==3.9.1 freezegun==0.3.11 google-cloud-core==1.0.2