summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVitor Freitas2019-01-13 21:03:23 +0200
committerVitor Freitas2019-01-13 21:03:23 +0200
commit9ace7627d5e9cd7ae314f0499b2d0f4a659111fb (patch)
tree8e3e5b6200feccebace8a95dea5ca67f2f805009
parentb8a0cf965307989f2749554353b73adcca023a33 (diff)
downloadcolossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.tar.gz
colossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.tar.bz2
colossus-9ace7627d5e9cd7ae314f0499b2d0f4a659111fb.zip
Add user timezone. Update account settings pages.
-rw-r--r--colossus/apps/accounts/forms.py21
-rw-r--r--colossus/apps/accounts/middleware.py19
-rw-r--r--colossus/apps/accounts/migrations/0002_user_timezone.py18
-rw-r--r--colossus/apps/accounts/models.py3
-rw-r--r--colossus/apps/accounts/templates/accounts/profile.html11
-rw-r--r--colossus/apps/accounts/templates/registration/base.html (renamed from colossus/templates/registration/base.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/logged_out.html (renamed from colossus/templates/registration/logged_out.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/login.html (renamed from colossus/templates/registration/login.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/password_change_done.html8
-rw-r--r--colossus/apps/accounts/templates/registration/password_change_form.html12
-rw-r--r--colossus/apps/accounts/templates/registration/password_reset_complete.html (renamed from colossus/templates/registration/password_reset_complete.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/password_reset_confirm.html (renamed from colossus/templates/registration/password_reset_confirm.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/password_reset_done.html (renamed from colossus/templates/registration/password_reset_done.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/password_reset_email.html (renamed from colossus/templates/registration/password_reset_email.html)0
-rw-r--r--colossus/apps/accounts/templates/registration/password_reset_form.html (renamed from colossus/templates/registration/password_reset_form.html)0
-rw-r--r--colossus/apps/accounts/tests/test_forms.py23
-rw-r--r--colossus/apps/accounts/tests/test_views.py78
-rw-r--r--colossus/apps/accounts/urls.py22
-rw-r--r--colossus/apps/accounts/views.py17
-rw-r--r--colossus/apps/campaigns/templates/campaigns/schedule_campaign_form.html38
-rw-r--r--colossus/apps/campaigns/views.py4
-rw-r--r--colossus/apps/core/templates/core/settings.html13
-rw-r--r--colossus/apps/core/templates/core/site_form.html2
-rw-r--r--colossus/settings.py1
-rw-r--r--colossus/templates/registration/password_change_done.html16
-rw-r--r--colossus/templates/registration/password_change_form.html20
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 %}