Account management
This commit is contained in:
parent
94f4ed6327
commit
b2829716b0
61 changed files with 1311 additions and 21 deletions
|
|
@ -13,6 +13,9 @@ export const CORE_DIR = path.join(PROJECT_DIR, 'news', 'core', 'static', 'core')
|
||||||
|
|
||||||
const taskMappings = [
|
const taskMappings = [
|
||||||
{ name: 'login', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` },
|
{ name: 'login', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` },
|
||||||
|
{ name: 'register', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` },
|
||||||
|
{ name: 'activate', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` },
|
||||||
|
{ name: 'password-reset', destDir: `${ACCOUNTS_DIR}/${STATIC_SUFFIX}` },
|
||||||
{ name: 'homepage', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
{ name: 'homepage', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
||||||
{ name: 'categories', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
{ name: 'categories', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
||||||
{ name: 'category', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
{ name: 'category', destDir: `${CORE_DIR}/${STATIC_SUFFIX}` },
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ Django==2.2
|
||||||
django-celery-beat==1.5.0
|
django-celery-beat==1.5.0
|
||||||
djangorestframework==3.9.4
|
djangorestframework==3.9.4
|
||||||
django-rest-swagger==2.2.0
|
django-rest-swagger==2.2.0
|
||||||
|
django-registration-redux==2.6
|
||||||
lxml==4.3.4
|
lxml==4.3.4
|
||||||
feedparser==5.2.1
|
feedparser==5.2.1
|
||||||
idna==2.8
|
idna==2.8
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,18 @@
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<form class="login-form" method="POST" action="{% url 'accounts:login' %}">
|
<form class="login-form" method="POST" action="{% url 'accounts:login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<h4>Login</h4>
|
<div class="form__header">
|
||||||
|
<h1 class="form__title">Login</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset class="login-form__fieldset">
|
<fieldset class="login-form__fieldset">
|
||||||
{{ form }}
|
{{ form }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="login-form__fieldset">
|
<fieldset class="login-form__fieldset">
|
||||||
<button class="button button--confirm" type="submit">Login</button>
|
<button class="button button--confirm" type="submit">Login</button>
|
||||||
<a><small>I forgot my password</small></a>
|
<a class="link" href="{% url 'accounts:password-reset' %}">
|
||||||
|
<small class="small">I forgot my password</small>
|
||||||
|
</a>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,20 @@
|
||||||
|
import hashlib
|
||||||
|
import string
|
||||||
|
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
|
from registration.models import RegistrationProfile
|
||||||
|
|
||||||
from newsreader.accounts.models import User
|
from newsreader.accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
def get_activation_key():
|
||||||
|
random_string = get_random_string(length=32, allowed_chars=string.printable)
|
||||||
|
return hashlib.sha1(random_string.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(factory.django.DjangoModelFactory):
|
class UserFactory(factory.django.DjangoModelFactory):
|
||||||
email = factory.Faker("email")
|
email = factory.Faker("email")
|
||||||
password = factory.Faker("password")
|
password = factory.Faker("password")
|
||||||
|
|
@ -17,3 +29,11 @@ class UserFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationProfileFactory(factory.django.DjangoModelFactory):
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
activation_key = factory.LazyFunction(get_activation_key)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RegistrationProfile
|
||||||
|
|
|
||||||
102
src/newsreader/accounts/tests/test_activation.py
Normal file
102
src/newsreader/accounts/tests/test_activation.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from registration.models import RegistrationProfile
|
||||||
|
|
||||||
|
from newsreader.accounts.models import User
|
||||||
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class ActivationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.register_url = reverse("accounts:register")
|
||||||
|
self.register_success_url = reverse("accounts:register-complete")
|
||||||
|
self.success_url = reverse("accounts:activate-complete")
|
||||||
|
|
||||||
|
def test_activation(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.register_url, data)
|
||||||
|
self.assertRedirects(response, self.register_success_url)
|
||||||
|
|
||||||
|
register_profile = RegistrationProfile.objects.get()
|
||||||
|
|
||||||
|
kwargs = {"activation_key": register_profile.activation_key}
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
def test_expired_key(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.register_url, data)
|
||||||
|
|
||||||
|
register_profile = RegistrationProfile.objects.get()
|
||||||
|
user = register_profile.user
|
||||||
|
|
||||||
|
user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
kwargs = {"activation_key": register_profile.activation_key}
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertContains(response, _("Account activation failed"))
|
||||||
|
|
||||||
|
user.refresh_from_db()
|
||||||
|
self.assertFalse(user.is_active)
|
||||||
|
|
||||||
|
def test_invalid_key(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.register_url, data)
|
||||||
|
self.assertRedirects(response, self.register_success_url)
|
||||||
|
|
||||||
|
kwargs = {"activation_key": "not-a-valid-key"}
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertContains(response, _("Account activation failed"))
|
||||||
|
|
||||||
|
user = User.objects.get()
|
||||||
|
|
||||||
|
self.assertEquals(user.is_active, False)
|
||||||
|
|
||||||
|
def test_activated_key(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.register_url, data)
|
||||||
|
self.assertRedirects(response, self.register_success_url)
|
||||||
|
|
||||||
|
register_profile = RegistrationProfile.objects.get()
|
||||||
|
|
||||||
|
kwargs = {"activation_key": register_profile.activation_key}
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
# try this a second time
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
164
src/newsreader/accounts/tests/test_password_reset.py
Normal file
164
src/newsreader/accounts/tests/test_password_reset.py
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from django.contrib.auth.tokens import default_token_generator as token_generator
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.http import urlsafe_base64_encode
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from newsreader.accounts.models import User
|
||||||
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.url = reverse("accounts:password-reset")
|
||||||
|
self.success_url = reverse("accounts:password-reset-done")
|
||||||
|
self.user = UserFactory(email="test@test.com")
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_password_change(self):
|
||||||
|
data = {"email": "test@test.com"}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
def test_unkown_email(self):
|
||||||
|
data = {"email": "unknown@test.com"}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
def test_repeatedly(self):
|
||||||
|
data = {"email": "test@test.com"}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetConfirmTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.success_url = reverse("accounts:password-reset-complete")
|
||||||
|
self.user = UserFactory(email="test@test.com")
|
||||||
|
|
||||||
|
def _get_reset_credentials(self) -> Dict:
|
||||||
|
data = {"email": self.user.email}
|
||||||
|
|
||||||
|
response = self.client.post(reverse("accounts:password-reset"), data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uidb64": response.context[0]["uid"],
|
||||||
|
"token": response.context[0]["token"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
kwargs = self._get_reset_credentials()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRedirects(
|
||||||
|
response, f"/accounts/password-reset/{kwargs['uidb64']}/set-password/"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_confirm_password(self):
|
||||||
|
kwargs = self._get_reset_credentials()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"}
|
||||||
|
|
||||||
|
response = self.client.post(response.url, data)
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertTrue(self.user.check_password("jabbadabadoe"))
|
||||||
|
|
||||||
|
def test_wrong_uuid(self):
|
||||||
|
correct_kwargs = self._get_reset_credentials()
|
||||||
|
wrong_kwargs = {"uidb64": "burp", "token": correct_kwargs["token"]}
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=wrong_kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, _("Password reset unsuccessful"))
|
||||||
|
|
||||||
|
def test_wrong_token(self):
|
||||||
|
correct_kwargs = self._get_reset_credentials()
|
||||||
|
wrong_kwargs = {"uidb64": correct_kwargs["uidb64"], "token": "token"}
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=wrong_kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, _("Password reset unsuccessful"))
|
||||||
|
|
||||||
|
def test_wrong_url_args(self):
|
||||||
|
kwargs = {"uidb64": "burp", "token": "token"}
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, _("Password reset unsuccessful"))
|
||||||
|
|
||||||
|
def test_token_repeatedly(self):
|
||||||
|
kwargs = self._get_reset_credentials()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"}
|
||||||
|
|
||||||
|
self.client.post(response.url, data)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, _("Password reset unsuccessful"))
|
||||||
|
|
||||||
|
def test_change_form_repeatedly(self):
|
||||||
|
kwargs = self._get_reset_credentials()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {"new_password1": "new-password", "new_password2": "new-password"}
|
||||||
|
|
||||||
|
self.client.post(response.url, data)
|
||||||
|
|
||||||
|
data = {"new_password1": "jabbadabadoe", "new_password2": "jabbadabadoe"}
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("accounts:password-reset-confirm", kwargs=kwargs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, _("Password reset unsuccessful"))
|
||||||
|
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertTrue(self.user.check_password("new-password"))
|
||||||
110
src/newsreader/accounts/tests/test_registration.py
Normal file
110
src/newsreader/accounts/tests/test_registration.py
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TransactionTestCase as TestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from registration.models import RegistrationProfile
|
||||||
|
|
||||||
|
from newsreader.accounts.models import User
|
||||||
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.url = reverse("accounts:register")
|
||||||
|
self.success_url = reverse("accounts:register-complete")
|
||||||
|
self.disallowed_url = reverse("accounts:register-closed")
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_registration(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.assertEquals(User.objects.count(), 1)
|
||||||
|
self.assertEquals(RegistrationProfile.objects.count(), 1)
|
||||||
|
|
||||||
|
user = User.objects.get()
|
||||||
|
|
||||||
|
self.assertEquals(user.is_active, False)
|
||||||
|
self.assertEquals(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
def test_existing_email(self):
|
||||||
|
UserFactory(email="test@test.com")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertEquals(User.objects.count(), 1)
|
||||||
|
self.assertContains(response, _("User with this Email address already exists"))
|
||||||
|
|
||||||
|
def test_pending_registration(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
self.assertEquals(User.objects.count(), 1)
|
||||||
|
self.assertEquals(RegistrationProfile.objects.count(), 1)
|
||||||
|
|
||||||
|
user = User.objects.get()
|
||||||
|
|
||||||
|
self.assertEquals(user.is_active, False)
|
||||||
|
self.assertEquals(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
self.assertContains(response, _("User with this Email address already exists"))
|
||||||
|
|
||||||
|
def test_disabled_account(self):
|
||||||
|
UserFactory(email="test@test.com", is_active=False)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertEquals(User.objects.count(), 1)
|
||||||
|
self.assertContains(response, _("User with this Email address already exists"))
|
||||||
|
|
||||||
|
@override_settings(REGISTRATION_OPEN=False)
|
||||||
|
def test_registration_closed(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.disallowed_url)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, data)
|
||||||
|
self.assertRedirects(response, self.disallowed_url)
|
||||||
|
|
||||||
|
self.assertEquals(User.objects.count(), 0)
|
||||||
|
self.assertEquals(RegistrationProfile.objects.count(), 0)
|
||||||
80
src/newsreader/accounts/tests/test_resend_activation.py
Normal file
80
src/newsreader/accounts/tests/test_resend_activation.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TransactionTestCase as TestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from registration.models import RegistrationProfile
|
||||||
|
|
||||||
|
from newsreader.accounts.models import User
|
||||||
|
from newsreader.accounts.tests.factories import RegistrationProfileFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
class ResendActivationTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.url = reverse("accounts:activate-resend")
|
||||||
|
self.success_url = reverse("accounts:activate-complete")
|
||||||
|
self.register_url = reverse("accounts:register")
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_resent_form(self):
|
||||||
|
data = {
|
||||||
|
"email": "test@test.com",
|
||||||
|
"password1": "test12456",
|
||||||
|
"password2": "test12456",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(self.register_url, data)
|
||||||
|
|
||||||
|
register_profile = RegistrationProfile.objects.get()
|
||||||
|
original_kwargs = {"activation_key": register_profile.activation_key}
|
||||||
|
|
||||||
|
response = self.client.post(self.url, {"email": "test@test.com"})
|
||||||
|
|
||||||
|
self.assertContains(response, _("We have sent an email to"))
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 2)
|
||||||
|
|
||||||
|
register_profile.refresh_from_db()
|
||||||
|
|
||||||
|
kwargs = {"activation_key": register_profile.activation_key}
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=kwargs))
|
||||||
|
|
||||||
|
self.assertRedirects(response, self.success_url)
|
||||||
|
|
||||||
|
register_profile.refresh_from_db()
|
||||||
|
user = register_profile.user
|
||||||
|
|
||||||
|
self.assertEquals(user.is_active, True)
|
||||||
|
|
||||||
|
# test the old activation code
|
||||||
|
response = self.client.get(reverse("accounts:activate", kwargs=original_kwargs))
|
||||||
|
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
self.assertContains(response, _("Account activation failed"))
|
||||||
|
|
||||||
|
def test_existing_account(self):
|
||||||
|
user = UserFactory(is_active=True)
|
||||||
|
profile = RegistrationProfileFactory(user=user, activated=True)
|
||||||
|
|
||||||
|
response = self.client.post(self.url, {"email": user.email})
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
# default behaviour is to show success page but not send an email
|
||||||
|
self.assertContains(response, _("We have sent an email to"))
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
def test_no_account(self):
|
||||||
|
response = self.client.post(self.url, {"email": "fake@mail.com"})
|
||||||
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
|
# default behaviour is to show success page but not send an email
|
||||||
|
self.assertContains(response, _("We have sent an email to"))
|
||||||
|
|
||||||
|
self.assertEquals(len(mail.outbox), 0)
|
||||||
|
|
@ -1,9 +1,54 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from newsreader.accounts.views import LoginView, LogoutView
|
from newsreader.accounts.views import (
|
||||||
|
ActivationCompleteView,
|
||||||
|
ActivationResendView,
|
||||||
|
ActivationView,
|
||||||
|
LoginView,
|
||||||
|
LogoutView,
|
||||||
|
PasswordResetCompleteView,
|
||||||
|
PasswordResetConfirmView,
|
||||||
|
PasswordResetDoneView,
|
||||||
|
PasswordResetView,
|
||||||
|
RegistrationClosedView,
|
||||||
|
RegistrationCompleteView,
|
||||||
|
RegistrationView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("login/", LoginView.as_view(), name="login"),
|
path("login/", LoginView.as_view(), name="login"),
|
||||||
path("logout/", LogoutView.as_view(), name="logout"),
|
path("logout/", LogoutView.as_view(), name="logout"),
|
||||||
|
path("register/", RegistrationView.as_view(), name="register"),
|
||||||
|
path(
|
||||||
|
"register/complete/", RegistrationCompleteView.as_view(), name="register-complete"
|
||||||
|
),
|
||||||
|
path("register/closed/", RegistrationClosedView.as_view(), name="register-closed"),
|
||||||
|
path(
|
||||||
|
"activate/complete/", ActivationCompleteView.as_view(), name="activate-complete"
|
||||||
|
),
|
||||||
|
path("activate/resend/", ActivationResendView.as_view(), name="activate-resend"),
|
||||||
|
path(
|
||||||
|
# This URL should be placed after all activate/ url's (see arg)
|
||||||
|
"activate/<str:activation_key>/",
|
||||||
|
ActivationView.as_view(),
|
||||||
|
name="activate",
|
||||||
|
),
|
||||||
|
path("password-reset/", PasswordResetView.as_view(), name="password-reset"),
|
||||||
|
path(
|
||||||
|
"password-reset/done/",
|
||||||
|
PasswordResetDoneView.as_view(),
|
||||||
|
name="password-reset-done",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"password-reset/<uidb64>/<token>/",
|
||||||
|
PasswordResetConfirmView.as_view(),
|
||||||
|
name="password-reset-confirm",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"password-reset/done/",
|
||||||
|
PasswordResetCompleteView.as_view(),
|
||||||
|
name="password-reset-complete",
|
||||||
|
),
|
||||||
|
# TODO: create password change views
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,91 @@
|
||||||
from django.contrib.auth.views import LoginView as DjangoLoginView
|
from django.contrib.auth import views as django_views
|
||||||
from django.contrib.auth.views import LogoutView as DjangoLogoutView
|
from django.shortcuts import render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
|
from registration.backends.default import views as registration_views
|
||||||
|
|
||||||
|
|
||||||
class LoginView(DjangoLoginView):
|
class LoginView(django_views.LoginView):
|
||||||
template_name = "accounts/login.html"
|
template_name = "accounts/login.html"
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("index")
|
return reverse_lazy("index")
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(DjangoLogoutView):
|
class LogoutView(django_views.LogoutView):
|
||||||
next_page = reverse_lazy("accounts:login")
|
next_page = reverse_lazy("accounts:login")
|
||||||
|
|
||||||
|
|
||||||
|
# RegistrationView shows a registration form and sends the email
|
||||||
|
# RegistrationCompleteView shows after filling in the registration form
|
||||||
|
# ActivationView is send within the activation email and activates the account
|
||||||
|
# ActivationCompleteView shows the success screen when activation was succesful
|
||||||
|
# ActivationResendView can be used when activation links are expired
|
||||||
|
# RegistrationClosedView shows when registration is disabled
|
||||||
|
class RegistrationView(registration_views.RegistrationView):
|
||||||
|
disallowed_url = reverse_lazy("accounts:register-closed")
|
||||||
|
template_name = "registration/registration_form.html"
|
||||||
|
success_url = reverse_lazy("accounts:register-complete")
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationCompleteView(TemplateView):
|
||||||
|
template_name = "registration/registration_complete.html"
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationClosedView(TemplateView):
|
||||||
|
template_name = "registration/registration_closed.html"
|
||||||
|
|
||||||
|
|
||||||
|
# Redirects or renders failed activation template
|
||||||
|
class ActivationView(registration_views.ActivationView):
|
||||||
|
template_name = "registration/activation_failure.html"
|
||||||
|
|
||||||
|
def get_success_url(self, user):
|
||||||
|
return ("accounts:activate-complete", (), {})
|
||||||
|
|
||||||
|
|
||||||
|
class ActivationCompleteView(TemplateView):
|
||||||
|
template_name = "registration/activation_complete.html"
|
||||||
|
|
||||||
|
|
||||||
|
# Renders activation form resend or resend_activation_complete
|
||||||
|
class ActivationResendView(registration_views.ResendActivationView):
|
||||||
|
template_name = "registration/activation_resend_form.html"
|
||||||
|
|
||||||
|
def render_form_submitted_template(self, form):
|
||||||
|
"""
|
||||||
|
Renders resend activation complete template with the submitted email.
|
||||||
|
|
||||||
|
"""
|
||||||
|
email = form.cleaned_data["email"]
|
||||||
|
context = {"email": email}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
self.request, "registration/activation_resend_complete.html", context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# PasswordResetView sends the mail
|
||||||
|
# PasswordResetDoneView shows a success message for the above
|
||||||
|
# PasswordResetConfirmView checks the link the user clicked and
|
||||||
|
# prompts for a new password
|
||||||
|
# PasswordResetCompleteView shows a success message for the above
|
||||||
|
class PasswordResetView(django_views.PasswordResetView):
|
||||||
|
template_name = "password-reset/password_reset_form.html"
|
||||||
|
subject_template_name = "password-reset/password_reset_subject.txt"
|
||||||
|
email_template_name = "password-reset/password_reset_email.html"
|
||||||
|
success_url = reverse_lazy("accounts:password-reset-done")
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetDoneView(django_views.PasswordResetDoneView):
|
||||||
|
template_name = "password-reset/password_reset_done.html"
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetConfirmView(django_views.PasswordResetConfirmView):
|
||||||
|
template_name = "password-reset/password_reset_confirm.html"
|
||||||
|
success_url = reverse_lazy("accounts:password-reset-complete")
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetCompleteView(django_views.PasswordResetCompleteView):
|
||||||
|
template_name = "password-reset/password_reset_complete.html"
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ INSTALLED_APPS = [
|
||||||
"rest_framework_swagger",
|
"rest_framework_swagger",
|
||||||
"celery",
|
"celery",
|
||||||
"django_celery_beat",
|
"django_celery_beat",
|
||||||
|
"registration",
|
||||||
# app modules
|
# app modules
|
||||||
"newsreader.accounts",
|
"newsreader.accounts",
|
||||||
"newsreader.news.core",
|
"newsreader.news.core",
|
||||||
|
|
@ -137,3 +138,7 @@ SWAGGER_SETTINGS = {
|
||||||
"LOGOUT_URL": "rest_framework:logout",
|
"LOGOUT_URL": "rest_framework:logout",
|
||||||
"DOC_EXPANSION": "list",
|
"DOC_EXPANSION": "list",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REGISTRATION_OPEN = True
|
||||||
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
REGISTRATION_AUTO_LOGIN = True
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "core/category.html" %}
|
{% extends "core/category.html" %}
|
||||||
|
|
||||||
{% block form-header %}
|
{% block form-header %}
|
||||||
<h1 class="h1">Create a category</h1>
|
<h1 class="h1 form__title">Create a category</h1>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block name-input %}
|
{% block name-input %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "core/category.html" %}
|
{% extends "core/category.html" %}
|
||||||
|
|
||||||
{% block form-header %}
|
{% block form-header %}
|
||||||
<h1 class="h1">Update category</h1>
|
<h1 class="h1 form__title">Update category</h1>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block name-input %}
|
{% block name-input %}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@
|
||||||
|
|
||||||
font-family: $default-font;
|
font-family: $default-font;
|
||||||
color: $default-font-color;
|
color: $default-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@extend .body;
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/newsreader/scss/components/fieldset/_fieldset.scss
Normal file
11
src/newsreader/scss/components/fieldset/_fieldset.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
@extend .fieldset;
|
||||||
|
}
|
||||||
1
src/newsreader/scss/components/fieldset/index.scss
Normal file
1
src/newsreader/scss/components/fieldset/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
@import "fieldset";
|
||||||
|
|
@ -8,11 +8,7 @@
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
|
|
||||||
&__fieldset {
|
&__fieldset {
|
||||||
display: flex;
|
@extend .fieldset;
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
padding: 15px;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
|
|
@ -22,7 +18,15 @@
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
& .favicon {
|
& .favicon {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
@extend .form;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@
|
||||||
@import "./messages/index";
|
@import "./messages/index";
|
||||||
@import "./section/index";
|
@import "./section/index";
|
||||||
@import "./errorlist/index";
|
@import "./errorlist/index";
|
||||||
|
@import "./fieldset/index";
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@extend .list;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
a {
|
|
||||||
@extend .link;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
@import "a";
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
.help-text {
|
.help-text {
|
||||||
@extends .small;
|
@extend .small;
|
||||||
|
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.helptext {
|
||||||
|
@extend .help-text;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
@import "./button/index";
|
@import "button/index";
|
||||||
@import "link/index";
|
@import "link/index";
|
||||||
@import "a/index";
|
|
||||||
@import "h1/index";
|
@import "h1/index";
|
||||||
@import "h2/index";
|
@import "h2/index";
|
||||||
@import "h3/index";
|
@import "h3/index";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
.input {
|
.input {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
|
background-color: lighten($gainsboro, +4%);
|
||||||
border: 1px $border-gray solid;
|
border: 1px $border-gray solid;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
|
|
@ -8,3 +9,7 @@
|
||||||
border: 1px $focus-blue solid;
|
border: 1px $focus-blue solid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@extend .input;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
.label {
|
.label {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@extend .label;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@extend .link;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
.small {
|
.small {
|
||||||
color: $nickel;
|
color: $nickel;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
@extend .small;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
src/newsreader/scss/pages/activate/components/index.scss
Normal file
0
src/newsreader/scss/pages/activate/components/index.scss
Normal file
0
src/newsreader/scss/pages/activate/elements/index.scss
Normal file
0
src/newsreader/scss/pages/activate/elements/index.scss
Normal file
8
src/newsreader/scss/pages/activate/index.scss
Normal file
8
src/newsreader/scss/pages/activate/index.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// General imports
|
||||||
|
@import "../../partials/variables";
|
||||||
|
@import "../../components/index";
|
||||||
|
@import "../../elements/index";
|
||||||
|
|
||||||
|
// Page specific
|
||||||
|
@import "./components/index";
|
||||||
|
@import "./elements/index";
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "password-reset-form/index";
|
||||||
|
@import "password-reset-confirm-form/index";
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.password-reset-confirm-form {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@import "password-reset-confirm-form";
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
.password-reset-form {
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&__fieldset:last-child {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .form__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .form__title {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@import "password-reset-form";
|
||||||
8
src/newsreader/scss/pages/password-reset/index.scss
Normal file
8
src/newsreader/scss/pages/password-reset/index.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// General imports
|
||||||
|
@import "../../partials/variables";
|
||||||
|
@import "../../components/index";
|
||||||
|
@import "../../elements/index";
|
||||||
|
|
||||||
|
// Page specific
|
||||||
|
@import "./components/index";
|
||||||
|
@import "./elements/index";
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
.activation-form {
|
||||||
|
margin: 10px 0;
|
||||||
|
& h4 {
|
||||||
|
padding: 20px 24px 5px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fieldset:last-child {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@import "activation-form";
|
||||||
2
src/newsreader/scss/pages/register/components/index.scss
Normal file
2
src/newsreader/scss/pages/register/components/index.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "register-form/index";
|
||||||
|
@import "activation-form/index";
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
.register-form {
|
||||||
|
margin: 10px 0;
|
||||||
|
& h4 {
|
||||||
|
padding: 20px 24px 5px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fieldset:last-child {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@import "register-form";
|
||||||
0
src/newsreader/scss/pages/register/elements/index.scss
Normal file
0
src/newsreader/scss/pages/register/elements/index.scss
Normal file
8
src/newsreader/scss/pages/register/index.scss
Normal file
8
src/newsreader/scss/pages/register/index.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// General imports
|
||||||
|
@import "../../partials/variables";
|
||||||
|
@import "../../components/index";
|
||||||
|
@import "../../elements/index";
|
||||||
|
|
||||||
|
// Page specific
|
||||||
|
@import "./components/index";
|
||||||
|
@import "./elements/index";
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<li class="nav__item"><a href="{% url 'accounts:logout' %}">Logout</a></li>
|
<li class="nav__item"><a href="{% url 'accounts:logout' %}">Logout</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="nav__item"><a href="{% url 'accounts:login' %}">Login</a></li>
|
<li class="nav__item"><a href="{% url 'accounts:login' %}">Login</a></li>
|
||||||
|
<li class="nav__item"><a href="{% url 'accounts:register' %}">Register</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
28
src/newsreader/templates/password-reset/password_reset_complete.html
Executable file
28
src/newsreader/templates/password-reset/password_reset_complete.html
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Password reset complete" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/password-reset.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Password reset complete" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% trans "Your password has been reset!" %}
|
||||||
|
{% blocktrans %}
|
||||||
|
You may now <a href="{{ login_url }}">log in</a>
|
||||||
|
{% endblocktrans %}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
59
src/newsreader/templates/password-reset/password_reset_confirm.html
Executable file
59
src/newsreader/templates/password-reset/password_reset_confirm.html
Executable file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block meta %}
|
||||||
|
<!-- NOTE(joshblum): This prevents leaking the password reset token via the
|
||||||
|
Referer header to any 3rd party apps on the page. -->
|
||||||
|
<meta name="referrer" content="origin">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Confirm password reset" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/password-reset.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
|
||||||
|
{% if validlink %}
|
||||||
|
<form class="form password-reset-confirm-form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form__header">
|
||||||
|
<h1 class="form__title">
|
||||||
|
{% trans "Enter your new password below to reset your password:" %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset password-reset-form__fieldset">
|
||||||
|
{{ form }}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset password-reset-form__fieldset">
|
||||||
|
<a class="button button--cancel" href="{% url 'accounts:login' %}">Cancel</a>
|
||||||
|
<button class="button button--confirm" type="submit">Change password</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Password reset unsuccessful" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% url 'accounts:password-reset' as reset_url %}
|
||||||
|
{% blocktrans %}
|
||||||
|
Password reset unsuccessful. Please
|
||||||
|
<a class="link" href="{{ reset_url }}">try again.</a>
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{# This is used by django.contrib.auth #}
|
||||||
27
src/newsreader/templates/password-reset/password_reset_done.html
Executable file
27
src/newsreader/templates/password-reset/password_reset_done.html
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Password reset" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/password-reset.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Password reset" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
We have sent you an email with a link to reset your password. Please check
|
||||||
|
your email and click the link to continue.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
30
src/newsreader/templates/password-reset/password_reset_email.html
Executable file
30
src/newsreader/templates/password-reset/password_reset_email.html
Executable file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% blocktrans %}Greetings{% endblocktrans %} {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user }}{% endif %},
|
||||||
|
|
||||||
|
{% blocktrans %}
|
||||||
|
You are receiving this email because you (or someone pretending to be you)
|
||||||
|
requested that your password be reset on the {{ domain }} site. If you do not
|
||||||
|
wish to reset your password, please ignore this message.
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
|
{% blocktrans %}
|
||||||
|
To reset your password, please click the following link, or copy and paste it
|
||||||
|
into your web browser:
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
|
<a rel="noreferrer" href="{{ protocol }}://{{ domain }}{% url 'accounts:password-reset-confirm' uid token %}">
|
||||||
|
{{ protocol }}://{{ domain }}{% url 'accounts:password-reset-confirm' uid token %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% blocktrans %}
|
||||||
|
Your username, in case you've forgotten:
|
||||||
|
{% endblocktrans %} {{ user.get_username }}
|
||||||
|
|
||||||
|
{% blocktrans %}
|
||||||
|
Best regards
|
||||||
|
{% endblocktrans %},
|
||||||
|
{{ site_name }}
|
||||||
|
{% blocktrans %}
|
||||||
|
Management
|
||||||
|
{% endblocktrans %}
|
||||||
34
src/newsreader/templates/password-reset/password_reset_form.html
Executable file
34
src/newsreader/templates/password-reset/password_reset_form.html
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Reset password" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/password-reset.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<form class="form password-reset-form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form__header">
|
||||||
|
<h1 class="form__title">{% trans "Reset password" %}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
Forgot your password? Enter your email in the form below and we'll send you
|
||||||
|
instructions for creating a new one.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset password-reset-form__fieldset">
|
||||||
|
{{ form }}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset password-reset-form__fieldset">
|
||||||
|
<a class="button button--cancel" href="{% url 'accounts:login' %}">Cancel</a>
|
||||||
|
<button class="button button--confirm" type="submit">Send reset mail</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% load i18n %}{% trans "Password reset on" %} {{ site_name }}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
See the save method in the PasswordChangeForm in django/contrib/auth/forms.py
|
||||||
|
for the available context data
|
||||||
|
{% endcomment %}
|
||||||
35
src/newsreader/templates/registration/activation_complete.html
Executable file
35
src/newsreader/templates/registration/activation_complete.html
Executable file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Account Activated" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/activate.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/activation_complete.html**
|
||||||
|
|
||||||
|
Used after successful account activation. This template has no context
|
||||||
|
variables of its own, and should simply inform the user that their
|
||||||
|
account is now active.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Account activated" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% trans "Your account is now activated." %}
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
{% trans "You can log in." %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
72
src/newsreader/templates/registration/activation_email.html
Normal file
72
src/newsreader/templates/registration/activation_email.html
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{{ site.name }} {% trans "registration" %}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
{% blocktrans with site_name=site.name %}
|
||||||
|
You (or someone pretending to be you) have asked to register an account at
|
||||||
|
{{ site_name }}. If this wasn't you, please ignore this email
|
||||||
|
and your address will be removed from our records.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
To activate this account, please click the following link within the next
|
||||||
|
{{ expiration_days }} days:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="http://{{site.domain}}{% url 'accounts:activate' activation_key %}">
|
||||||
|
{{site.domain}}{% url 'accounts:activate' activation_key %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans with site_name=site.name %}
|
||||||
|
Sincerely,
|
||||||
|
{{ site_name }} Management
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/activation_email.html**
|
||||||
|
|
||||||
|
Used to generate the html body of the activation email. Should display a
|
||||||
|
link the user can click to activate the account. This template has the
|
||||||
|
following context:
|
||||||
|
|
||||||
|
``activation_key``
|
||||||
|
The activation key for the new account.
|
||||||
|
|
||||||
|
``expiration_days``
|
||||||
|
The number of days remaining during which the account may be
|
||||||
|
activated.
|
||||||
|
|
||||||
|
``site``
|
||||||
|
An object representing the site on which the user registered;
|
||||||
|
depending on whether ``django.contrib.sites`` is installed, this
|
||||||
|
may be an instance of either ``django.contrib.sites.models.Site``
|
||||||
|
(if the sites application is installed) or
|
||||||
|
``django.contrib.sites.requests.RequestSite`` (if not). Consult `the
|
||||||
|
documentation for the Django sites framework
|
||||||
|
<http://docs.djangoproject.com/en/dev/ref/contrib/sites/>`_ for
|
||||||
|
details regarding these objects' interfaces.
|
||||||
|
|
||||||
|
``user``
|
||||||
|
The new user account
|
||||||
|
|
||||||
|
``request``
|
||||||
|
``HttpRequest`` instance for better flexibility.
|
||||||
|
For example it can be used to compute absolute register URL:
|
||||||
|
|
||||||
|
{{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %}
|
||||||
|
{% endcomment %}
|
||||||
52
src/newsreader/templates/registration/activation_email.txt
Normal file
52
src/newsreader/templates/registration/activation_email.txt
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans with site_name=site.name %}
|
||||||
|
You (or someone pretending to be you) have asked to register an account at
|
||||||
|
{{ site_name }}. If this wasn't you, please ignore this email
|
||||||
|
and your address will be removed from our records.
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% blocktrans %}
|
||||||
|
To activate this account, please click the following link within the next
|
||||||
|
{{ expiration_days }} days:
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
|
http://{{site.domain}}{% url 'accounts:activate' activation_key %}
|
||||||
|
|
||||||
|
{% blocktrans with site_name=site.name %}
|
||||||
|
Sincerely,
|
||||||
|
{{ site_name }} Management
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/activation_email.txt**
|
||||||
|
|
||||||
|
Used to generate the text body of the activation email. Should display a
|
||||||
|
link the user can click to activate the account. This template has the
|
||||||
|
following context:
|
||||||
|
|
||||||
|
``activation_key``
|
||||||
|
The activation key for the new account.
|
||||||
|
|
||||||
|
``expiration_days``
|
||||||
|
The number of days remaining during which the account may be
|
||||||
|
activated.
|
||||||
|
|
||||||
|
``site``
|
||||||
|
An object representing the site on which the user registered;
|
||||||
|
depending on whether ``django.contrib.sites`` is installed, this
|
||||||
|
may be an instance of either ``django.contrib.sites.models.Site``
|
||||||
|
(if the sites application is installed) or
|
||||||
|
``django.contrib.sites.requests.RequestSite`` (if not). Consult `the
|
||||||
|
documentation for the Django sites framework
|
||||||
|
<http://docs.djangoproject.com/en/dev/ref/contrib/sites/>`_ for
|
||||||
|
details regarding these objects' interfaces.
|
||||||
|
|
||||||
|
``user``
|
||||||
|
The new user account
|
||||||
|
|
||||||
|
``request``
|
||||||
|
``HttpRequest`` instance for better flexibility.
|
||||||
|
For example it can be used to compute absolute register URL:
|
||||||
|
|
||||||
|
{{ request.scheme }}://{{ request.get_host }}{% url 'registration_activate' activation_key %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% load i18n %}{% trans "Account activation on" %} {{ site.name }}
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/activation_email_subject.txt**
|
||||||
|
|
||||||
|
Used to generate the subject line of the activation email. Because the
|
||||||
|
subject line of an email must be a single line of text, any output
|
||||||
|
from this template will be forcibly condensed to a single line before
|
||||||
|
being used. This template has the following context:
|
||||||
|
|
||||||
|
``activation_key``
|
||||||
|
The activation key for the new account.
|
||||||
|
|
||||||
|
``expiration_days``
|
||||||
|
The number of days remaining during which the account may be
|
||||||
|
activated.
|
||||||
|
|
||||||
|
``site``
|
||||||
|
An object representing the site on which the user registered;
|
||||||
|
depending on whether ``django.contrib.sites`` is installed, this
|
||||||
|
may be an instance of either ``django.contrib.sites.models.Site``
|
||||||
|
(if the sites application is installed) or
|
||||||
|
``django.contrib.sites.requests.RequestSite`` (if not). Consult `the
|
||||||
|
documentation for the Django sites framework
|
||||||
|
<http://docs.djangoproject.com/en/dev/ref/contrib/sites/>`_ for
|
||||||
|
details regarding these objects' interfaces.
|
||||||
|
{% endcomment %}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Activation Failure" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/activate.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/activate.html**
|
||||||
|
|
||||||
|
Used if account activation fails. With the default setup, has the following context:
|
||||||
|
|
||||||
|
``activation_key``
|
||||||
|
The activation key used during the activation attempt.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Activation Failure" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>{% trans "Account activation failed." %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Account Activation Resent" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/register.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/resend_activation_complete.html**
|
||||||
|
Used after form for resending account activation is submitted. By default has
|
||||||
|
the following context:
|
||||||
|
|
||||||
|
``email``
|
||||||
|
The email address submitted in the resend activation form.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Account activation resent" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
We have sent an email to {{ email }} with further instructions.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Resend Activation Email" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/register.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/resend_activation_form.html**
|
||||||
|
Used to show the form users will fill out to resend the activation email. By
|
||||||
|
default, has the following context:
|
||||||
|
|
||||||
|
``form``
|
||||||
|
The registration form. This will be an instance of some subclass
|
||||||
|
of ``django.forms.Form``; consult `Django's forms documentation
|
||||||
|
<http://docs.djangoproject.com/en/dev/topics/forms/>`_ for
|
||||||
|
information on how to display this in a template.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<form class="form activation-form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form__header">
|
||||||
|
<h1 class="form__title">Resend activation code</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset activation-form__fieldset">
|
||||||
|
{{ form }}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset activation-form__fieldset">
|
||||||
|
<a class="button button--cancel" href="{% url 'accounts:login' %}">Cancel</a>
|
||||||
|
<button class="button button--confirm" type="submit">Resend code</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
25
src/newsreader/templates/registration/registration_closed.html
Executable file
25
src/newsreader/templates/registration/registration_closed.html
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Registration is closed" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/register.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Registration is closed" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% trans "Sorry, but registration is closed at this moment. Come back later." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
33
src/newsreader/templates/registration/registration_complete.html
Executable file
33
src/newsreader/templates/registration/registration_complete.html
Executable file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Activation email sent" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/register.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
**registration/registration_complete.html**
|
||||||
|
|
||||||
|
Used after successful completion of the registration form. This
|
||||||
|
template has no context variables of its own, and should simply inform
|
||||||
|
the user that an email containing account-activation information has
|
||||||
|
been sent.
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<h1>{% trans "Activation email sent" %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="card__content">
|
||||||
|
<p>
|
||||||
|
{% trans "Please check your email to complete the registration process." %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card__footer" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
26
src/newsreader/templates/registration/registration_form.html
Normal file
26
src/newsreader/templates/registration/registration_form.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link href="{% static 'accounts/dist/css/register.css' %}" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="content">
|
||||||
|
<form class="form register-form" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form__header">
|
||||||
|
<h1 class="form__title">Register</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset register-form__fieldset">
|
||||||
|
{{ form }}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset register-form__fieldset">
|
||||||
|
<a class="button button--cancel" href="{% url 'accounts:login' %}">Cancel</a>
|
||||||
|
<button class="button button--confirm" type="submit">Register</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue