diff options
author | Vitor Freitas | 2019-01-13 21:03:23 +0200 |
---|---|---|
committer | Vitor Freitas | 2019-01-13 21:03:23 +0200 |
commit | 9ace7627d5e9cd7ae314f0499b2d0f4a659111fb (patch) | |
tree | 8e3e5b6200feccebace8a95dea5ca67f2f805009 | |
parent | b8a0cf965307989f2749554353b73adcca023a33 (diff) | |
download | colossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.tar.gz colossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.tar.bz2 colossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.zip |
Add user timezone. Update account settings pages.
26 files changed, 275 insertions, 51 deletions
diff --git a/colossus/apps/accounts/forms.py b/colossus/apps/accounts/forms.py index 015f188..42271ad 100644 --- a/colossus/apps/accounts/forms.py +++ b/colossus/apps/accounts/forms.py @@ -1,7 +1,10 @@ -from django.contrib.auth import get_user_model +from django import forms from django.contrib.auth.forms import UserCreationForm +from django.utils.translation import gettext_lazy as _ -User = get_user_model() +import pytz + +from .models import User class AdminUserCreationForm(UserCreationForm): @@ -16,3 +19,17 @@ class AdminUserCreationForm(UserCreationForm): if commit: user.save() return user + + +class UserForm(forms.ModelForm): + TIMEZONE_CHOICES = (('', '---------'),) + tuple(map(lambda tz: (tz, tz), pytz.common_timezones)) + + timezone = forms.ChoiceField( + choices=TIMEZONE_CHOICES, + required=False, + label=_('Timezone') + ) + + class Meta: + model = User + fields = ('first_name', 'last_name', 'email', 'timezone') diff --git a/colossus/apps/accounts/middleware.py b/colossus/apps/accounts/middleware.py new file mode 100644 index 0000000..744ae7c --- /dev/null +++ b/colossus/apps/accounts/middleware.py @@ -0,0 +1,19 @@ +from django.utils import timezone + +import pytz +from pytz import UnknownTimeZoneError + + +class UserTimezoneMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: + try: + timezone.activate(pytz.timezone(request.user.timezone)) + except UnknownTimeZoneError: + timezone.deactivate() + + response = self.get_response(request) + return response diff --git a/colossus/apps/accounts/migrations/0002_user_timezone.py b/colossus/apps/accounts/migrations/0002_user_timezone.py new file mode 100644 index 0000000..12020b1 --- /dev/null +++ b/colossus/apps/accounts/migrations/0002_user_timezone.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-01-13 16:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='timezone', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/colossus/apps/accounts/models.py b/colossus/apps/accounts/models.py index 44eba0c..d4ef2b7 100644 --- a/colossus/apps/accounts/models.py +++ b/colossus/apps/accounts/models.py @@ -1,6 +1,9 @@ from django.contrib.auth.models import AbstractUser +from django.db import models class User(AbstractUser): + timezone = models.CharField(max_length=50, blank=True) + class Meta: db_table = 'auth_user' diff --git a/colossus/apps/accounts/templates/accounts/profile.html b/colossus/apps/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..04af980 --- /dev/null +++ b/colossus/apps/accounts/templates/accounts/profile.html @@ -0,0 +1,11 @@ +{% extends 'core/settings.html' %} + +{% load crispy_forms_filters i18n %} + +{% block settingscontent %} + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + <button type="submit" class="btn btn-success">{% trans 'Save changes' %}</button> + </form> +{% endblock %} diff --git a/colossus/templates/registration/base.html b/colossus/apps/accounts/templates/registration/base.html index 4a2dd19..4a2dd19 100644 --- a/colossus/templates/registration/base.html +++ b/colossus/apps/accounts/templates/registration/base.html diff --git a/colossus/templates/registration/logged_out.html b/colossus/apps/accounts/templates/registration/logged_out.html index f8b4827..f8b4827 100644 --- a/colossus/templates/registration/logged_out.html +++ b/colossus/apps/accounts/templates/registration/logged_out.html diff --git a/colossus/templates/registration/login.html b/colossus/apps/accounts/templates/registration/login.html index f467249..f467249 100644 --- a/colossus/templates/registration/login.html +++ b/colossus/apps/accounts/templates/registration/login.html diff --git a/colossus/apps/accounts/templates/registration/password_change_done.html b/colossus/apps/accounts/templates/registration/password_change_done.html new file mode 100644 index 0000000..8ea3abc --- /dev/null +++ b/colossus/apps/accounts/templates/registration/password_change_done.html @@ -0,0 +1,8 @@ +{% extends 'core/settings.html' %} + +{% load i18n %} + +{% block settingscontent %} + <h2 class="card-title">{% trans 'Change password' %}</h2> + <p class="card-text">{% trans 'Password changed with success!' %}</p> +{% endblock %} diff --git a/colossus/apps/accounts/templates/registration/password_change_form.html b/colossus/apps/accounts/templates/registration/password_change_form.html new file mode 100644 index 0000000..0a0eab3 --- /dev/null +++ b/colossus/apps/accounts/templates/registration/password_change_form.html @@ -0,0 +1,12 @@ +{% extends 'core/settings.html' %} + +{% load crispy_forms_filters i18n %} + +{% block settingscontent %} + <h2 class="card-title">{% trans 'Change password' %}</h2> + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + <button type="submit" class="btn btn-success">{% trans 'Change password' %}</button> + </form> +{% endblock %} diff --git a/colossus/templates/registration/password_reset_complete.html b/colossus/apps/accounts/templates/registration/password_reset_complete.html index 8bd2400..8bd2400 100644 --- a/colossus/templates/registration/password_reset_complete.html +++ b/colossus/apps/accounts/templates/registration/password_reset_complete.html diff --git a/colossus/templates/registration/password_reset_confirm.html b/colossus/apps/accounts/templates/registration/password_reset_confirm.html index b538956..b538956 100644 --- a/colossus/templates/registration/password_reset_confirm.html +++ b/colossus/apps/accounts/templates/registration/password_reset_confirm.html diff --git a/colossus/templates/registration/password_reset_done.html b/colossus/apps/accounts/templates/registration/password_reset_done.html index 980b22f..980b22f 100644 --- a/colossus/templates/registration/password_reset_done.html +++ b/colossus/apps/accounts/templates/registration/password_reset_done.html diff --git a/colossus/templates/registration/password_reset_email.html b/colossus/apps/accounts/templates/registration/password_reset_email.html index 37467b8..37467b8 100644 --- a/colossus/templates/registration/password_reset_email.html +++ b/colossus/apps/accounts/templates/registration/password_reset_email.html diff --git a/colossus/templates/registration/password_reset_form.html b/colossus/apps/accounts/templates/registration/password_reset_form.html index 5c67895..5c67895 100644 --- a/colossus/templates/registration/password_reset_form.html +++ b/colossus/apps/accounts/templates/registration/password_reset_form.html diff --git a/colossus/apps/accounts/tests/test_forms.py b/colossus/apps/accounts/tests/test_forms.py new file mode 100644 index 0000000..d65d8f9 --- /dev/null +++ b/colossus/apps/accounts/tests/test_forms.py @@ -0,0 +1,23 @@ +from colossus.apps.accounts.forms import UserForm +from colossus.apps.accounts.tests.factories import UserFactory +from colossus.test.testcases import TestCase + + +class UserFormTests(TestCase): + def setUp(self): + self.user = UserFactory() + self.data = { + 'first_name': 'John', + 'last_name': 'Doe', + 'email': 'john.doe@example.com', + 'timezone': 'Europe/Helsinki' + } + + def test_invalid_timezone(self): + self.data['timezone'] = 'xxx' + form = UserForm(instance=self.user, data=self.data) + self.assertFalse(form.is_valid()) + + def test_valid_timezone(self): + form = UserForm(instance=self.user, data=self.data) + self.assertTrue(form.is_valid()) diff --git a/colossus/apps/accounts/tests/test_views.py b/colossus/apps/accounts/tests/test_views.py new file mode 100644 index 0000000..635a33b --- /dev/null +++ b/colossus/apps/accounts/tests/test_views.py @@ -0,0 +1,78 @@ +from django.urls import resolve, reverse + +from colossus.apps.accounts import forms, views +from colossus.test.testcases import AuthenticatedTestCase, TestCase + + +class AccountsLoginRequiredTests(TestCase): + """ + Test if all the urls from accounts' app are protected with login_required decorator + Perform a GET request to all urls. The expected outcome is a redirection + to the login page. + """ + def test_redirection(self): + patterns = [ + 'password_change', + 'password_change_done', + 'profile' + ] + for url_name in patterns: + with self.subTest(url_name=url_name): + url = reverse(url_name) + response = self.client.get(url) + self.assertRedirectsLoginRequired(response, url) + + +class ProfileViewTests(AuthenticatedTestCase): + def setUp(self): + super().setUp() + url = reverse('profile') + self.response = self.client.get(url) + + def test_status_code_200(self): + self.assertEqual(self.response.status_code, 200) + + def test_url_resolves_correct_view(self): + view = resolve('/accounts/profile/') + self.assertEqual(view.func.view_class, views.ProfileView) + + def test_csrf(self): + self.assertContains(self.response, 'csrfmiddlewaretoken') + + def test_form(self): + form = self.response.context.get('form') + self.assertIsInstance(form, forms.UserForm) + + def test_html_content(self): + contents = ( + ('<input type="hidden"', 1), + ('<input type="text"', 2), + ('<input type="email"', 1), + ('<select', 1) + ) + for content in contents: + with self.subTest(content=content[0]): + self.assertContains(self.response, content[0], content[1]) + + +class ProfileViewSuccessTests(AuthenticatedTestCase): + def setUp(self): + super().setUp() + self.url = reverse('profile') + data = { + 'first_name': 'John', + 'last_name': 'Doe', + 'email': 'john.doe@example.com', + 'timezone': 'UTC' + } + self.response = self.client.post(self.url, data) + + def test_created_campaign(self): + self.user.refresh_from_db() + self.assertEqual('John', self.user.first_name) + self.assertEqual('Doe', self.user.last_name) + self.assertEqual('john.doe@example.com', self.user.email) + self.assertEqual('UTC', self.user.timezone) + + def test_redirection(self): + self.assertRedirects(self.response, self.url) diff --git a/colossus/apps/accounts/urls.py b/colossus/apps/accounts/urls.py index 62dd710..c243b45 100644 --- a/colossus/apps/accounts/urls.py +++ b/colossus/apps/accounts/urls.py @@ -1,15 +1,19 @@ -from django.contrib.auth import views +from django.contrib.auth import views as auth_views from django.urls import path +from . import views + urlpatterns = [ - path('login/', views.LoginView.as_view(), name='login'), - path('logout/', views.LogoutView.as_view(), name='logout'), + path('login/', auth_views.LoginView.as_view(), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + + path('password-change/', auth_views.PasswordChangeView.as_view(), name='password_change'), + path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), - path('password-change/', views.PasswordChangeView.as_view(), name='password_change'), - path('password-change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'), + path('password-reset/', auth_views.PasswordResetView.as_view(), name='password_reset'), + path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), + path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), - path('password-reset/', views.PasswordResetView.as_view(), name='password_reset'), - path('password-reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'), - path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), - path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), + path('profile/', views.ProfileView.as_view(), name='profile'), ] diff --git a/colossus/apps/accounts/views.py b/colossus/apps/accounts/views.py index e69de29..eaa8d56 100644 --- a/colossus/apps/accounts/views.py +++ b/colossus/apps/accounts/views.py @@ -0,0 +1,17 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy +from django.views.generic import UpdateView + +from colossus.apps.accounts.forms import UserForm + +from .models import User + + +class ProfileView(LoginRequiredMixin, UpdateView): + model = User + form_class = UserForm + template_name = 'accounts/profile.html' + success_url = reverse_lazy('profile') + + def get_object(self, queryset=None): + return self.request.user diff --git a/colossus/apps/campaigns/templates/campaigns/schedule_campaign_form.html b/colossus/apps/campaigns/templates/campaigns/schedule_campaign_form.html new file mode 100644 index 0000000..9a55699 --- /dev/null +++ b/colossus/apps/campaigns/templates/campaigns/schedule_campaign_form.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% load crispy_forms_filters i18n tz %} + +{% block title %}{% trans 'Schedule campaign' %}{% endblock %} + +{% block javascript %} + <script> + $(function () { + + }); + </script> +{% endblock %} + +{% block content %} + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="{% url 'campaigns:campaigns' %}">{% trans 'Campaigns' %}</a></li> + <li class="breadcrumb-item"><a href="{% url 'campaigns:campaign_edit' campaign.pk %}">{{ campaign.name }}</a></li> + <li class="breadcrumb-item active" aria-current="page">{% trans 'Schedule campaign' %}</li> + </ol> + </nav> + <div class="card mb-3"> + <div class="card-body"> + <h2 class="card-title">{% trans 'Schedule campaign' %}</h2> + <form method="post" novalidate> + {% csrf_token %} + {{ form|crispy }} + <div class="form-group"> + {% get_current_timezone as TIME_ZONE %} + <small class="text-muted">Current time: {{ time|localtime }}<br>Current time zone: {{ TIME_ZONE }}</small> + </div> + <button type="submit" class="btn btn-success" role="button">{% trans 'Schedule' %}</button> + <a href="{{ campaign.get_absolute_url }}" class="btn btn-outline-secondary" role="button">{% trans 'Never mind' %}</a> + </form> + </div> + </div> +{% endblock %} diff --git a/colossus/apps/campaigns/views.py b/colossus/apps/campaigns/views.py index 50986ee..a35923c 100644 --- a/colossus/apps/campaigns/views.py +++ b/colossus/apps/campaigns/views.py @@ -6,6 +6,7 @@ from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.urls import reverse, reverse_lazy +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.safestring import mark_safe from django.utils.translation import gettext, gettext_lazy as _ @@ -356,12 +357,13 @@ class ScheduleCampaignView(CampaignMixin, UpdateView): model = Campaign context_object_name = 'campaign' form_class = ScheduleCampaignForm + template_name = 'campaigns/schedule_campaign_form.html' def get_queryset(self): return super().get_queryset().filter(status__in={CampaignStatus.DRAFT, CampaignStatus.SCHEDULED}) def get_context_data(self, **kwargs): - kwargs['title'] = _('Schedule campaign') + kwargs['time'] = timezone.now() return super().get_context_data(**kwargs) diff --git a/colossus/apps/core/templates/core/settings.html b/colossus/apps/core/templates/core/settings.html index 917447a..bd1c732 100644 --- a/colossus/apps/core/templates/core/settings.html +++ b/colossus/apps/core/templates/core/settings.html @@ -13,12 +13,21 @@ <div class="row"> <div class="col-4"> - <div class="card"> + <div class="card mb-3"> + <div class="card-header"> + {% trans 'Personal settings' %} + </div> + <nav class="list-group list-group-flush"> + <a href="{% url 'profile' %}" class="list-group-item list-group-item-action">{% trans 'Profile' %}</a> + <a href="{% url 'password_change' %}" class="list-group-item list-group-item-action">{% trans 'Change password' %}</a> + </nav> + </div> + <div class="card mb-3"> <div class="card-header"> {% trans 'Application settings' %} </div> <nav class="list-group list-group-flush"> - <a href="" class="list-group-item list-group-item-action active">Site domain</a> + <a href="{% url 'settings' %}" class="list-group-item list-group-item-action">{% trans 'Site domain' %}</a> </nav> </div> </div> diff --git a/colossus/apps/core/templates/core/site_form.html b/colossus/apps/core/templates/core/site_form.html index 2259441..8fc20d8 100644 --- a/colossus/apps/core/templates/core/site_form.html +++ b/colossus/apps/core/templates/core/site_form.html @@ -1,6 +1,6 @@ {% extends 'core/settings.html' %} -{% load crispy_forms_tags i18n %} +{% load crispy_forms_filters i18n %} {% block settingscontent %} <h2 class="card-title">{% trans 'Site domain' %}</h2> diff --git a/colossus/settings.py b/colossus/settings.py index 157e28f..147ec51 100644 --- a/colossus/settings.py +++ b/colossus/settings.py @@ -71,6 +71,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'colossus.apps.accounts.middleware.UserTimezoneMiddleware', ] diff --git a/colossus/templates/registration/password_change_done.html b/colossus/templates/registration/password_change_done.html deleted file mode 100644 index 3d25abe..0000000 --- a/colossus/templates/registration/password_change_done.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'base.html' %} - -{% load i18n %} - -{% block content %} - <div class="row justify-content-center"> - <div class="col-md-10 col-lg-8"> - <div class="card border-success"> - <div class="card-body"> - <h4 class="card-title">{% trans 'Change password' %}</h4> - <p class="card-text">Password changed with success!</p> - </div> - </div> - </div> - </div> -{% endblock %} diff --git a/colossus/templates/registration/password_change_form.html b/colossus/templates/registration/password_change_form.html deleted file mode 100644 index 3874d28..0000000 --- a/colossus/templates/registration/password_change_form.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends 'base.html' %} - -{% load crispy_forms_tags i18n %} - -{% block content %} - <div class="row justify-content-center"> - <div class="col-md-10 col-lg-8"> - <div class="card"> - <div class="card-body"> - <h4 class="card-title">{% trans 'Change password' %}</h4> - <form method="post"> - {% csrf_token %} - {{ form|crispy }} - <button type="submit" class="btn btn-success">{% trans 'Change password' %}</button> - </form> - </div> - </div> - </div> - </div> -{% endblock %} |