From e74fd18f6c9472ee2c55272fcb966ad2cdb8f8a7 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Mon, 10 Sep 2012 22:57:45 -0700 Subject: [PATCH 001/239] Relax character requirements for usernames --- account/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account/forms.py b/account/forms.py index 076a5a10..eb738df0 100644 --- a/account/forms.py +++ b/account/forms.py @@ -18,7 +18,7 @@ from account.models import EmailAddress -alnum_re = re.compile(r"^\w+$") +alnum_re = re.compile(r"^[\w\-\.\+]+$") class SignupForm(forms.Form): @@ -49,7 +49,7 @@ class SignupForm(forms.Form): def clean_username(self): if not alnum_re.search(self.cleaned_data["username"]): - raise forms.ValidationError(_("Usernames can only contain letters, numbers and underscores.")) + raise forms.ValidationError(_("Usernames can only contain letters, numbers and the following special characters ./+/-/_")) User = get_user_model() lookup_kwargs = get_user_lookup_kwargs({ "{username}__iexact": self.cleaned_data["username"] From 09ca7dd9326b375bcedb6db3862ce930419246f4 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Mon, 10 Sep 2012 22:58:22 -0700 Subject: [PATCH 002/239] Signup code use should default to 1 --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index 1d09b343..218c8dd2 100644 --- a/account/models.py +++ b/account/models.py @@ -129,7 +129,7 @@ class InvalidCode(Exception): pass code = models.CharField(max_length=64, unique=True) - max_uses = models.PositiveIntegerField(default=0) + max_uses = models.PositiveIntegerField(default=1) expiry = models.DateTimeField(null=True, blank=True) inviter = models.ForeignKey(AUTH_USER_MODEL, null=True, blank=True) email = models.EmailField(blank=True) From 0130e7921cc7dbf35d2edb6008f399ee60be5162 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Fri, 8 Feb 2013 09:27:56 -0800 Subject: [PATCH 003/239] RFC3696/5321 require max_length for email address of 254 --- account/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account/models.py b/account/models.py index 218c8dd2..aed81d57 100644 --- a/account/models.py +++ b/account/models.py @@ -132,7 +132,7 @@ class InvalidCode(Exception): max_uses = models.PositiveIntegerField(default=1) expiry = models.DateTimeField(null=True, blank=True) inviter = models.ForeignKey(AUTH_USER_MODEL, null=True, blank=True) - email = models.EmailField(blank=True) + email = models.EmailField(blank=True, max_length=254) notes = models.TextField(blank=True) sent = models.DateTimeField(null=True, blank=True) created = models.DateTimeField(default=timezone.now, editable=False) @@ -235,7 +235,7 @@ def save(self, **kwargs): class EmailAddress(models.Model): user = models.ForeignKey(AUTH_USER_MODEL) - email = models.EmailField(unique=settings.ACCOUNT_EMAIL_UNIQUE) + email = models.EmailField(unique=settings.ACCOUNT_EMAIL_UNIQUE, max_length=254) verified = models.BooleanField(default=False) primary = models.BooleanField(default=False) @@ -341,7 +341,7 @@ def send(self, **kwargs): class AccountDeletion(models.Model): user = models.ForeignKey(AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - email = models.EmailField() + email = models.EmailField(max_length=254) date_requested = models.DateTimeField(default=timezone.now) date_expunged = models.DateTimeField(null=True, blank=True) From 0abe60dac8369db0fb166f01ecdd15eac75a186a Mon Sep 17 00:00:00 2001 From: Rob LaRubbio Date: Tue, 5 Jan 2016 12:57:09 -0800 Subject: [PATCH 004/239] Check the email against the email address, not the code --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index 12408fa6..898182be 100644 --- a/account/models.py +++ b/account/models.py @@ -158,7 +158,7 @@ def exists(cls, code=None, email=None): if code: checks.append(Q(code=code)) if email: - checks.append(Q(email=code)) + checks.append(Q(email=email)) if not checks: return False return cls._default_manager.filter(six.moves.reduce(operator.or_, checks)).exists() From b171eb0c77f2d68051b48145f4e49275ed6860b9 Mon Sep 17 00:00:00 2001 From: Rob LaRubbio Date: Wed, 6 Jan 2016 15:48:24 -0800 Subject: [PATCH 005/239] Add tests for signup code exists method --- account/tests/test_models.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 account/tests/test_models.py diff --git a/account/tests/test_models.py b/account/tests/test_models.py new file mode 100644 index 00000000..5a5b114c --- /dev/null +++ b/account/tests/test_models.py @@ -0,0 +1,50 @@ +from django.conf import settings +from django.core import mail +from django.core.urlresolvers import reverse +from django.test import TestCase, override_settings + +from django.contrib.auth.models import User + +from account.models import SignupCode + + +class SignupCodeModelTestCase(TestCase): + def test_exists_no_match(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertFalse(SignupCode.exists(code='BARBAR')) + self.assertFalse(SignupCode.exists(email='bar@example.com')) + self.assertFalse(SignupCode.exists(email='bar@example.com', code='BARBAR')) + self.assertFalse(SignupCode.exists()) + + def test_exists_email_only_match(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertTrue(SignupCode.exists(email='foobar@example.com')) + + def test_exists_code_only_match(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertTrue(SignupCode.exists(code='FOOFOO')) + self.assertTrue(SignupCode.exists(email='bar@example.com', code='FOOFOO')) + + def test_exists_email_match_code_mismatch(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertTrue(SignupCode.exists(email='foobar@example.com', code='BARBAR')) + + def test_exists_code_match_email_mismatch(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertTrue(SignupCode.exists(email='bar@example.com', code='FOOFOO')) + + def test_exists_both_match(self): + code = SignupCode(email='foobar@example.com', code='FOOFOO') + code.save() + + self.assertTrue(SignupCode.exists(email='foobar@example.com', code='FOOFOO')) From 631deb14e2c7d20ff8bc370dfc7df53dc6b0fefe Mon Sep 17 00:00:00 2001 From: Rob LaRubbio Date: Wed, 6 Jan 2016 15:51:02 -0800 Subject: [PATCH 006/239] Resolve flake errors --- account/tests/test_models.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/account/tests/test_models.py b/account/tests/test_models.py index 5a5b114c..dd91315d 100644 --- a/account/tests/test_models.py +++ b/account/tests/test_models.py @@ -1,9 +1,4 @@ -from django.conf import settings -from django.core import mail -from django.core.urlresolvers import reverse -from django.test import TestCase, override_settings - -from django.contrib.auth.models import User +from django.test import TestCase from account.models import SignupCode From 526bcda633448e6801a95bdd33d7e0d5eaad0406 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 18 Feb 2016 11:17:08 -0600 Subject: [PATCH 007/239] Port callback functions into hookset Closes #106 --- account/callbacks.py | 10 ---------- account/conf.py | 8 -------- account/hooks.py | 7 +++++++ 3 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 account/callbacks.py diff --git a/account/callbacks.py b/account/callbacks.py deleted file mode 100644 index d54213fc..00000000 --- a/account/callbacks.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import unicode_literals - - -def account_delete_mark(deletion): - deletion.user.is_active = False - deletion.user.save() - - -def account_delete_expunge(deletion): - deletion.user.delete() diff --git a/account/conf.py b/account/conf.py index 66cd7b33..7d35e079 100644 --- a/account/conf.py +++ b/account/conf.py @@ -49,19 +49,11 @@ class AccountAppConf(AppConf): EMAIL_CONFIRMATION_URL = "account_confirm_email" SETTINGS_REDIRECT_URL = "account_settings" NOTIFY_ON_PASSWORD_CHANGE = True - DELETION_MARK_CALLBACK = "account.callbacks.account_delete_mark" - DELETION_EXPUNGE_CALLBACK = "account.callbacks.account_delete_expunge" DELETION_EXPUNGE_HOURS = 48 HOOKSET = "account.hooks.AccountDefaultHookSet" TIMEZONES = TIMEZONES LANGUAGES = LANGUAGES USE_AUTH_AUTHENTICATE = False - def configure_deletion_mark_callback(self, value): - return load_path_attr(value) - - def configure_deletion_expunge_callback(self, value): - return load_path_attr(value) - def configure_hookset(self, value): return load_path_attr(value)() diff --git a/account/hooks.py b/account/hooks.py index 4128b4cb..606eee73 100644 --- a/account/hooks.py +++ b/account/hooks.py @@ -53,6 +53,13 @@ def get_user_credentials(self, form, identifier_field): "password": form.cleaned_data["password"], } + def account_delete_mark(self, deletion): + deletion.user.is_active = False + deletion.user.save() + + def account_delete_expunge(self, deletion): + deletion.user.delete() + class HookProxy(object): From f867b21ba23b19f7ec69479438cf57484f31e572 Mon Sep 17 00:00:00 2001 From: Anna Ossowski Date: Tue, 15 Mar 2016 13:31:18 +0100 Subject: [PATCH 008/239] Update README.rst --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2adab254..e5dc72b8 100644 --- a/README.rst +++ b/README.rst @@ -77,16 +77,16 @@ Contribute See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/ including a video, or our How to Contribute (http://pinaxproject.com/pinax/how_to_contribute/) section for an overview on how contributing to Pinax works. For concrete contribution ideas, please see our Ways to Contribute/What We Need Help With (http://pinaxproject.com/pinax/ways_to_contribute/) section. -In case of any questions, we would recommend for you to join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. +In case of any questions, we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. -We would also highly recommend for your to read our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). +We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). Code of Conduct ----------------- In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. -We'd like to ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. +We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. From ca45e17425dbe8d2b24a377ec165c727c7b2f02d Mon Sep 17 00:00:00 2001 From: Anna Ossowski Date: Tue, 15 Mar 2016 13:34:38 +0100 Subject: [PATCH 009/239] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e5dc72b8..70825cb8 100644 --- a/README.rst +++ b/README.rst @@ -77,7 +77,7 @@ Contribute See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/ including a video, or our How to Contribute (http://pinaxproject.com/pinax/how_to_contribute/) section for an overview on how contributing to Pinax works. For concrete contribution ideas, please see our Ways to Contribute/What We Need Help With (http://pinaxproject.com/pinax/ways_to_contribute/) section. -In case of any questions, we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. +In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). From 35d7c537159391322f793e54c0fb82a0a6ebc76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serkan=20Altunta=C5=9F?= Date: Mon, 11 Apr 2016 22:11:06 +0300 Subject: [PATCH 010/239] Turkish Translation --- account/locale/tr/LC_MESSAGES/django.mo | Bin 0 -> 3570 bytes account/locale/tr/LC_MESSAGES/django.po | 216 ++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 account/locale/tr/LC_MESSAGES/django.mo create mode 100644 account/locale/tr/LC_MESSAGES/django.po diff --git a/account/locale/tr/LC_MESSAGES/django.mo b/account/locale/tr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..84e46953c290964b55e4621f19ef0ab724ec765b GIT binary patch literal 3570 zcmb7`OKcoT8Gs82u$XsXc`UFLEDw8=p4eFktI2~*;z?rdjBPniP!?&U@^sBit@|;l z?shT`IpxM4(MlkpwYY!~2+0!SLT(vL95^f>!~yYGE{F?z;E+9V;QM>p_Siu}lwAFF zRagD>zv>_V`RI{X6>XOOY5Moyrqo4we+LiR^bw^3I0HWkzX6|tx8R51tMHxhhw!uT zr|=2*Yxq9+d-zfKw`%?olzp=4Y4BnAc{mH-3$MaY!JF_rybDjm2T;y`3!Z{U-!(pG zq2dZW!~8d)?Efx&7XBQ*1pfl%+;i{dOt=J5MH$GS>hgF$OyCFLx2o|g@PmxM2am!Z z!;ioR5RvM2DE9uL;$I;us((QK)SEoyoJUaX`!^K7Ji#W}_au+^z^9?Co37>q_%g9L z1tm5=V3EYH8 z$e((|L(V_KLwK~Jh9bXKjlW)v+feL(35uK;mij@l^FG9N>e~>N)ORXwLDAzUP|p1& zl(_z?`u<0V$?ESA_m{o{Ws~@5y2zC#@i^R`W#Th*@qskaMVjP<L_ zuf9lsJH19fPCr3^h91zrL{|-6WY%k!X>WTj%dNMezU}h9&ihQ-*tkfWF!VOdYO2xc zq(zeJ%;vdEdYLW;p~-DnQy0^u>wIi-mnPM&%(601_1#j)J=PT7%iOZbvfHT-Ra2yo zpSx%1j!ATq=6cQQE@vUSWs+WDdR8sk+xvSb2YTA{j7w(Jp5)z^$G+&S=yb59TSU=N zclyW{^_L@SGOP39!0KUIc-^UV9aOs3)QXL5yoRsZR;~7}?xdmBcjb9c+~<-F6FEf5 zw*zj|CJ9fZUhj&M19Ud99oKa@+jv_V+)43HCm#!tpd==?|NmZ8F;$dGtxP9(8&;$; z+!HISB5zGN)VW!=NljNC==9SRM;JoTx1Mb-wwq~U)wN2ay_c*ERhoz!(w{)O9#9DD`uR1NPc27J3$$)Fr&XHTBiD33(o6xYLH1r z+nv$2UMn8CLr< zzExMM5|v0vlas>Hc1`WPR+wbn>7CbH*IU&r53R7f!EK^XR8W9M+ryM9Fi)AjVUxXG z6YqLDx9i^c86=ky$2dH#*UZkAC_apg4(({m?QG@Fk8Nu`e2{l-qIb4j96C{MOGG7Z z)9D_Ic&Vuia{d*QkGAoEstWIV8Wy@|AO1+vwOkZA&uuRI8}`J89hyqI4%5+= zFUHD5BLBeF$>10~r~k)p>RK5g%Fuf5y2vXI1h|wDn!eEwe?j0VzoYF0|L$zn^sfEl zqjjU#$c@|(whY6@#=mG{eisRYi=BF5B5y?OV-pn4&~gUJ+l{P0&UiT5CT}o;gL948 z%^n%X55Kt1t|6E(AIcl2Q}Qa3Bgj&`I}U9b7JeL1k2~Nf@1!LQBvdg@%*HB9!mf>X zN^KUW5OC2z>J4Yhvcm;9Glu^z(!?8`p;LktY(%xObIZAy>mlPowT3ehrbq>qMg?hA z_#z4mYM%(8+8gisO8&RmA04=BYI0P{swTvI+&eKU@1EK$qd~An+eA=tLlp(t#YMaq z@>nV}s};d?Nrx3Z;?6IjWW3^HkG69y**oz*DIA#+>fJ1i$06i$?@duP)HptJvDJS7 D;F0zq literal 0 HcmV?d00001 diff --git a/account/locale/tr/LC_MESSAGES/django.po b/account/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 00000000..cfdba40b --- /dev/null +++ b/account/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,216 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-04-11 21:57+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: account/forms.py:28 account/forms.py:108 +msgid "Username" +msgstr "Kullanıcı adı" + +#: account/forms.py:34 account/forms.py:80 +msgid "Password" +msgstr "Şifre" + +#: account/forms.py:38 +msgid "Password (again)" +msgstr "Şifre (tekrar)" + +#: account/forms.py:42 account/forms.py:123 account/forms.py:169 +#: account/forms.py:198 +msgid "Email" +msgstr "Eposta" + +#: account/forms.py:53 +msgid "Usernames can only contain letters, numbers and underscores." +msgstr "Kullanıcı adı sadece harfler, sayılar ve alt çizgiler içerebilir." + +#: account/forms.py:61 +msgid "This username is already taken. Please choose another." +msgstr "Bu kullanıcı adı daha önce alınmış. Lütfen başka bir tane seçiniz." + +#: account/forms.py:68 account/forms.py:218 +msgid "A user is registered with this email address." +msgstr "Bir kullanıcı bu eposta adresi ile kaydedildi." + +#: account/forms.py:73 account/forms.py:163 account/forms.py:192 +msgid "You must type the same password each time." +msgstr "Şifrenizi her seferinde aynı girmelisiniz." + +#: account/forms.py:84 +msgid "Remember Me" +msgstr "Beni Hatırla" + +#: account/forms.py:97 +msgid "This account is inactive." +msgstr "Bu hesap inaktif." + +#: account/forms.py:109 +msgid "The username and/or password you specified are not correct." +msgstr "Belirttiğiniz kullanıcı adı ve/veya şifre doğru değil." + +#: account/forms.py:124 +msgid "The email address and/or password you specified are not correct." +msgstr "Belirttiğiniz eposta adresi ve/veya şifre doğru değil." + +#: account/forms.py:139 +msgid "Current Password" +msgstr "Şuanki Şifre" + +#: account/forms.py:143 account/forms.py:181 +msgid "New Password" +msgstr "Yeni Şifre" + +#: account/forms.py:147 account/forms.py:185 +msgid "New Password (again)" +msgstr "Yeni Şifre (tekrar)" + +#: account/forms.py:157 +msgid "Please type your current password." +msgstr "Lütfen şimdiki şifrenizi giriniz." + +#: account/forms.py:174 +msgid "Email address can not be found." +msgstr "Eposta adresiniz bulunamıyor." + +#: account/forms.py:200 +msgid "Timezone" +msgstr "Zaman Dilimi" + +#: account/forms.py:206 +msgid "Language" +msgstr "Dil" + +#: account/models.py:36 +msgid "user" +msgstr "kullanıcı" + +#: account/models.py:37 +msgid "timezone" +msgstr "zaman dilimi" + +#: account/models.py:39 +msgid "language" +msgstr "dil" + +#: account/models.py:135 +msgid "code" +msgstr "kod" + +#: account/models.py:136 +msgid "max uses" +msgstr "maximum kullanım" + +#: account/models.py:137 +msgid "expiry" +msgstr "zaman aşımı" + +#: account/models.py:140 +msgid "notes" +msgstr "notlar" + +#: account/models.py:141 +msgid "sent" +msgstr "gönderildi" + +#: account/models.py:142 +msgid "created" +msgstr "oluşturuldu" + +#: account/models.py:143 +msgid "use count" +msgstr "kullanım sayısı" + +#: account/models.py:146 +msgid "signup code" +msgstr "kayıt kodu" + +#: account/models.py:147 +msgid "signup codes" +msgstr "kayıt kodları" + +#: account/models.py:254 +msgid "verified" +msgstr "onaylandı" + +#: account/models.py:255 +msgid "primary" +msgstr "birincil" + +#: account/models.py:260 +msgid "email address" +msgstr "eposta adresi" + +#: account/models.py:261 +msgid "email addresses" +msgstr "eposta adresleri" + +#: account/models.py:311 +msgid "email confirmation" +msgstr "eposta onayı" + +#: account/models.py:312 +msgid "email confirmations" +msgstr "eposta onayları" + +#: account/models.py:361 +msgid "date requested" +msgstr "istenen tarih" + +#: account/models.py:362 +msgid "date expunged" +msgstr "silinen tarih" + +#: account/models.py:365 +msgid "account deletion" +msgstr "hesap silinmesi" + +#: account/models.py:366 +msgid "account deletions" +msgstr "hespa silinmeleri" + +#: account/views.py:42 +#, python-brace-format +msgid "Confirmation email sent to {email}." +msgstr "Onay epostası {email} adresine yollandı." + +#: account/views.py:46 +#, python-brace-format +msgid "The code {code} is invalid." +msgstr "{code} kodu geçersiz." + +#: account/views.py:384 +#, python-brace-format +msgid "You have confirmed {email}." +msgstr "{email} adresini onayladınız." + +#: account/views.py:457 account/views.py:593 +msgid "Password successfully changed." +msgstr "Şifre başarıyla değiştirildi." + +#: account/views.py:684 +msgid "Account settings updated." +msgstr "Hesap ayarları güncelendi." + +#: account/views.py:768 +#, python-brace-format +msgid "" +"Your account is now inactive and your data will be expunged in the next " +"{expunge_hours} hours." +msgstr "Hesabınız inaktiftir ve verileriniz" +"{expunge_hours} saat sonra silinecektir." + From fb82407d682b61a33996262ede77d31cfa2f1762 Mon Sep 17 00:00:00 2001 From: Jonathan Potter Date: Tue, 10 May 2016 15:53:59 -0400 Subject: [PATCH 011/239] Use dynamic settings in migrations to match model. This prevents the Django migration autodetector from making new migrations when a user changes settings. --- account/migrations/0002_fix_str.py | 92 +----------------------------- 1 file changed, 3 insertions(+), 89 deletions(-) diff --git a/account/migrations/0002_fix_str.py b/account/migrations/0002_fix_str.py index f28cb525..864ed004 100644 --- a/account/migrations/0002_fix_str.py +++ b/account/migrations/0002_fix_str.py @@ -3,6 +3,7 @@ from django.db import migrations, models +from account.conf import settings import account.fields @@ -17,96 +18,9 @@ class Migration(migrations.Migration): model_name="account", name="language", field=models.CharField( - default="en-us", + default=settings.LANGUAGE_CODE, verbose_name="language", - choices=[ - ("af", "Afrikaans"), - ("ar", "العربيّة"), - ("ast", "asturian"), - ("az", "Azərbaycanca"), - ("bg", "български"), - ("be", "беларуская"), - ("bn", "বাংলা"), - ("br", "brezhoneg"), - ("bs", "bosanski"), - ("ca", "català"), - ("cs", "česky"), - ("cy", "Cymraeg"), - ("da", "dansk"), - ("de", "Deutsch"), - ("el", "Ελληνικά"), - ("en", "English"), - ("en-a", "Australian English"), - ("en-gb", "British English"), - ("eo", "Esperanto"), - ("es", "español"), - ("es-ar", "español de Argentina"), - ("es-mx", "español de Mexico"), - ("es-ni", "español de Nicaragua"), - ("es-ve", "español de Venezuela"), - ("et", "eesti"), - ("e", "Basque"), - ("fa", "فارسی"), - ("fi", "suomi"), - ("fr", "français"), - ("fy", "frysk"), - ("ga", "Gaeilge"), - ("gl", "galego"), - ("he", "עברית"), - ("hi", "Hindi"), - ("hr", "Hrvatski"), - ("h", "Magyar"), - ("ia", "Interlingua"), - ("id", "Bahasa Indonesia"), - ("io", "ido"), - ("is", "Íslenska"), - ("it", "italiano"), - ("ja", "日本語"), - ("ka", "ქართული"), - ("kk", "Қазақ"), - ("km", "Khmer"), - ("kn", "Kannada"), - ("ko", "한국어"), - ("lb", "Lëtzebuergesch"), - ("lt", "Lietuviškai"), - ("lv", "latvieš"), - ("mk", "Македонски"), - ("ml", "Malayalam"), - ("mn", "Mongolian"), - ("mr", "मराठी"), - ("my", "မြန်မာဘာသာ"), - ("nb", "norsk (bokmål)"), - ("ne", "नेपाली"), - ("nl", "Nederlands"), - ("nn", "norsk (nynorsk)"), - ("os", "Ирон"), - ("pa", "Punjabi"), - ("pl", "polski"), - ("pt", "Português"), - ("pt-br", "Português Brasileiro"), - ("ro", "Română"), - ("r", "Русский"), - ("sk", "slovenský"), - ("sl", "Slovenščina"), - ("sq", "shqip"), - ("sr", "српски"), - ("sr-latn", "srpski (latinica)"), - ("sv", "svenska"), - ("sw", "Kiswahili"), - ("ta", "தமிழ்"), - ("te", "తెలుగు"), - ("th", "ภาษาไทย"), - ("tr", "Türkçe"), - ("tt", "Татарча"), - ("udm", "Удмурт"), - ("uk", "Українська"), - ("ur", "اردو"), - ("vi", "Tiếng Việt"), - ("zh-cn", "简体中文"), - ("zh-hans", "简体中文"), - ("zh-hant", "繁體中文"), - ("zh-tw", "繁體中文") - ], + choices=settings.ACCOUNT_LANGUAGES, max_length=10 ), ), From 8039f1d77a4c570b7bef4862b8dd2bb121f7fdd8 Mon Sep 17 00:00:00 2001 From: steph Date: Wed, 22 Jun 2016 00:13:41 +0200 Subject: [PATCH 012/239] On post_save handler, disable treatment when raw=True (useful for manage.py loaddata) --- account/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/account/models.py b/account/models.py index 12408fa6..02c65921 100644 --- a/account/models.py +++ b/account/models.py @@ -102,6 +102,11 @@ def user_post_save(sender, **kwargs): We only run on user creation to avoid having to check for existence on each call to User.save. """ + + # Disable post_save during manage.py loaddata + if kwargs.get('raw', False): + return False + user, created = kwargs["instance"], kwargs["created"] disabled = getattr(user, "_disable_account_creation", not settings.ACCOUNT_CREATE_ON_SAVE) if created and not disabled: From 64ff3a2a09ca8b4fa331937e93d38dffde9ff184 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 22 Jul 2016 07:19:56 -0400 Subject: [PATCH 013/239] Bumped version for next version --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 94c34e63..27916c74 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "1.4.0.dev1" +__version__ = "2.0.0.dev1" From 2739e91b9e486e8427417a8a8883bf276b247653 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 22 Jul 2016 07:27:07 -0400 Subject: [PATCH 014/239] Updated CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f704c450..b7ba36f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # ChangeLog +BI indicates a backward incompatible change. Take caution when upgrading to a +version with these. Your code will need to be updated to continue working. + +## 2.0.0 + + * BI: account deletion callbacks moved to hooksets + ## 1.3.0 * added Python 3.5 and Django 1.9 compatibility From e8156512dce1c2603fcd9e8d95cab8c145ce3fba Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 22 Jul 2016 07:31:02 -0400 Subject: [PATCH 015/239] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ba36f7..f911aa07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ version with these. Your code will need to be updated to continue working. ## 2.0.0 * BI: account deletion callbacks moved to hooksets + * added Turkish translations + * fixed migration with language codes to dynamically set ## 1.3.0 From c4abfe2fd809f19aabe689251dfc7a1e61c97d36 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 22 Jul 2016 07:31:35 -0400 Subject: [PATCH 016/239] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f911aa07..60198cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ version with these. Your code will need to be updated to continue working. ## 2.0.0 * BI: account deletion callbacks moved to hooksets + * BI: dropped Django 1.7 support * added Turkish translations * fixed migration with language codes to dynamically set From b2691a69abaab6e1d1b0214321735287cba09711 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 22 Jul 2016 09:42:50 -0400 Subject: [PATCH 017/239] Fixed conditional which broken Django 1.10 --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index 6c7189cd..2b2daf70 100644 --- a/account/views.py +++ b/account/views.py @@ -201,7 +201,7 @@ def create_email_address(self, form, **kwargs): kwargs.setdefault("primary", True) kwargs.setdefault("verified", False) if self.signup_code: - kwargs["verified"] = self.signup_code.email and self.created_user.email == self.signup_code.email + kwargs["verified"] = self.created_user.email == self.signup_code.email if self.signup_code.email else False return EmailAddress.objects.add_email(self.created_user, self.created_user.email, **kwargs) def use_signup_code(self, user): From 8230824fb7bfbd78c50c3cf0407c07415b8b8dfd Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 1 Sep 2016 15:22:32 -0600 Subject: [PATCH 018/239] Refactor common password change views Bring common code into PasswordChangeMixin. --- account/views.py | 161 +++++++++++++++++++++-------------------------- 1 file changed, 72 insertions(+), 89 deletions(-) diff --git a/account/views.py b/account/views.py index 2b2daf70..8fcee4da 100644 --- a/account/views.py +++ b/account/views.py @@ -446,10 +446,24 @@ def after_confirmation(self, confirmation): user.save() -class ChangePasswordView(FormView): +class ChangePasswordMixin(): + """ + Mixin handling common elements of password change. + + Required attributes in inheriting class: + + form_password_field - example: "password" + fallback_url_setting - example: "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" + + Required methods in inheriting class: + + get_user() + change_password() + after_change_password() + get_redirect_field_name() + + """ - template_name = "account/password_change.html" - form_class = ChangePasswordForm redirect_field_name = "next" messages = { "password_changed": { @@ -458,27 +472,26 @@ class ChangePasswordView(FormView): } } - def get(self, *args, **kwargs): - if not self.request.user.is_authenticated(): - return redirect("account_password_reset") - return super(ChangePasswordView, self).get(*args, **kwargs) - - def post(self, *args, **kwargs): - if not self.request.user.is_authenticated(): - return HttpResponseForbidden() - return super(ChangePasswordView, self).post(*args, **kwargs) + def get_context_data(self, **kwargs): + ctx = super(ChangePasswordMixin, self).get_context_data(**kwargs) + redirect_field_name = self.get_redirect_field_name() + ctx.update({ + "redirect_field_name": redirect_field_name, + "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + }) + return ctx def change_password(self, form): - user = self.request.user - user.set_password(form.cleaned_data["password_new"]) + user = self.get_user() + user.set_password(form.cleaned_data[self.form_password_field]) user.save() # required on Django >= 1.7 to keep the user authenticated if hasattr(auth, "update_session_auth_hash"): auth.update_session_auth_hash(self.request, user) def after_change_password(self): - user = self.request.user - signals.password_changed.send(sender=ChangePasswordView, user=user) + user = self.get_user() + signals.password_changed.send(sender=self, user=user) if settings.ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE: self.send_email(user) if self.messages.get("password_changed"): @@ -488,38 +501,17 @@ def after_change_password(self): self.messages["password_changed"]["text"] ) - def get_form_kwargs(self): - """ - Returns the keyword arguments for instantiating the form. - """ - kwargs = {"user": self.request.user, "initial": self.get_initial()} - if self.request.method in ["POST", "PUT"]: - kwargs.update({ - "data": self.request.POST, - "files": self.request.FILES, - }) - return kwargs - def form_valid(self, form): self.change_password(form) self.after_change_password() return redirect(self.get_success_url()) - def get_context_data(self, **kwargs): - ctx = super(ChangePasswordView, self).get_context_data(**kwargs) - redirect_field_name = self.get_redirect_field_name() - ctx.update({ - "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), - }) - return ctx - def get_redirect_field_name(self): return self.redirect_field_name def get_success_url(self, fallback_url=None, **kwargs): if fallback_url is None: - fallback_url = settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL + fallback_url = getattr(settings, self.fallback_url_setting, None) kwargs.setdefault("redirect_field_name", self.get_redirect_field_name()) return default_redirect(self.request, fallback_url, **kwargs) @@ -534,6 +526,46 @@ def send_email(self, user): hookset.send_password_change_email([user.email], ctx) +class ChangePasswordView(ChangePasswordMixin, FormView): + + template_name = "account/password_change.html" + form_class = ChangePasswordForm + redirect_field_name = "next" + messages = { + "password_changed": { + "level": messages.SUCCESS, + "text": _("Password successfully changed.") + } + } + form_password_field = "password_new" + fallback_url_setting = "ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL" + + def get(self, *args, **kwargs): + if not self.request.user.is_authenticated(): + return redirect("account_password_reset") + return super(ChangePasswordView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + if not self.request.user.is_authenticated(): + return HttpResponseForbidden() + return super(ChangePasswordView, self).post(*args, **kwargs) + + def get_user(self): + return self.request.user + + def get_form_kwargs(self): + """ + Returns the keyword arguments for instantiating the form. + """ + kwargs = {"user": self.request.user, "initial": self.get_initial()} + if self.request.method in ["POST", "PUT"]: + kwargs.update({ + "data": self.request.POST, + "files": self.request.FILES, + }) + return kwargs + + class PasswordResetView(FormView): template_name = "account/password_reset.html" @@ -586,13 +618,8 @@ class PasswordResetTokenView(FormView): template_name_fail = "account/password_reset_token_fail.html" form_class = PasswordResetTokenForm token_generator = default_token_generator - redirect_field_name = "next" - messages = { - "password_changed": { - "level": messages.SUCCESS, - "text": _("Password successfully changed.") - }, - } + form_password_field = "password" + fallback_url_setting = "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" def get(self, request, **kwargs): form_class = self.get_form_class() @@ -604,46 +631,12 @@ def get(self, request, **kwargs): def get_context_data(self, **kwargs): ctx = super(PasswordResetTokenView, self).get_context_data(**kwargs) - redirect_field_name = self.get_redirect_field_name() ctx.update({ "uidb36": self.kwargs["uidb36"], "token": self.kwargs["token"], - "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), }) return ctx - def change_password(self, form): - user = self.get_user() - user.set_password(form.cleaned_data["password"]) - user.save() - - def after_change_password(self): - user = self.get_user() - signals.password_changed.send(sender=PasswordResetTokenView, user=user) - if settings.ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE: - self.send_email(user) - if self.messages.get("password_changed"): - messages.add_message( - self.request, - self.messages["password_changed"]["level"], - self.messages["password_changed"]["text"] - ) - - def form_valid(self, form): - self.change_password(form) - self.after_change_password() - return redirect(self.get_success_url()) - - def get_redirect_field_name(self): - return self.redirect_field_name - - def get_success_url(self, fallback_url=None, **kwargs): - if fallback_url is None: - fallback_url = settings.ACCOUNT_PASSWORD_RESET_REDIRECT_URL - kwargs.setdefault("redirect_field_name", self.get_redirect_field_name()) - return default_redirect(self.request, fallback_url, **kwargs) - def get_user(self): try: uid_int = base36_to_int(self.kwargs["uidb36"]) @@ -662,16 +655,6 @@ def token_fail(self): } return self.response_class(**response_kwargs) - def send_email(self, user): - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") - current_site = get_current_site(self.request) - ctx = { - "user": user, - "protocol": protocol, - "current_site": current_site, - } - hookset.send_password_change_email([user.email], ctx) - class SettingsView(LoginRequiredMixin, FormView): From a7718bc4e4a316970e7dba3020c96fae843078a1 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 1 Sep 2016 15:31:31 -0600 Subject: [PATCH 019/239] Change class to new-style --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index 8fcee4da..5357806b 100644 --- a/account/views.py +++ b/account/views.py @@ -446,7 +446,7 @@ def after_confirmation(self, confirmation): user.save() -class ChangePasswordMixin(): +class ChangePasswordMixin(object): """ Mixin handling common elements of password change. From c3342a50201af17cd58e58a77c811315d75eddf0 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 1 Sep 2016 17:51:22 -0600 Subject: [PATCH 020/239] Add PasswordHistory and PasswordExpiry Update views to save password upon signup, change pw, or reset pw. Update LoginView to check for expired password. Added ACCOUNT_PASSWORD_USE_HISTORY (True/False) for determining whether or not to check password expiration. Added ACCOUNT_PASSWORD_EXPIRY (positive int) for number of seconds until password expires from last time it was set. --- account/conf.py | 2 + .../0003_passwordexpiry_passwordhistory.py | 35 +++ account/models.py | 17 ++ account/utils.py | 31 +++ account/views.py | 220 +++++++++--------- docs/settings.rst | 10 + 6 files changed, 209 insertions(+), 106 deletions(-) create mode 100644 account/migrations/0003_passwordexpiry_passwordhistory.py diff --git a/account/conf.py b/account/conf.py index 7d35e079..0efa0d7b 100644 --- a/account/conf.py +++ b/account/conf.py @@ -37,6 +37,8 @@ class AccountAppConf(AppConf): LOGOUT_REDIRECT_URL = "/" PASSWORD_CHANGE_REDIRECT_URL = "account_password" PASSWORD_RESET_REDIRECT_URL = "account_login" + PASSWORD_EXPIRY = 0 + PASSWORD_USE_HISTORY = False REMEMBER_ME_EXPIRY = 60 * 60 * 24 * 365 * 10 USER_DISPLAY = lambda user: user.username # flake8: noqa CREATE_ON_SAVE = True diff --git a/account/migrations/0003_passwordexpiry_passwordhistory.py b/account/migrations/0003_passwordexpiry_passwordhistory.py new file mode 100644 index 00000000..359017e9 --- /dev/null +++ b/account/migrations/0003_passwordexpiry_passwordhistory.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-09-01 17:50 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('account', '0002_fix_str'), + ] + + operations = [ + migrations.CreateModel( + name='PasswordExpiry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('expiry', models.PositiveIntegerField(default=0)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='password_expiry', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + ), + migrations.CreateModel( + name='PasswordHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=255)), + ('timestamp', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_history', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/account/models.py b/account/models.py index 12408fa6..c8a04724 100644 --- a/account/models.py +++ b/account/models.py @@ -385,3 +385,20 @@ def mark(cls, user): account_deletion.save() settings.ACCOUNT_DELETION_MARK_CALLBACK(account_deletion) return account_deletion + + +class PasswordHistory(models.Model): + """ + Contains single password history for user. + """ + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history") + password = models.CharField(max_length=255) # encrypted password + timestamp = models.DateTimeField(auto_now=True) + + +class PasswordExpiry(models.Model): + """ + Holds the password expiration period for a single user. + """ + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="password_expiry", verbose_name=_("user")) + expiry = models.PositiveIntegerField(default=0) diff --git a/account/utils.py b/account/utils.py index c493320d..1c26cae4 100644 --- a/account/utils.py +++ b/account/utils.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import datetime import functools try: from urllib.parse import urlparse, urlunparse @@ -13,6 +14,10 @@ from django.contrib.auth import get_user_model from account.conf import settings +from .models import ( + PasswordHistory, + PasswordExpiry, +) def get_user_lookup_kwargs(kwargs): @@ -106,3 +111,29 @@ def get_form_data(form, field_name, default=None): else: key = field_name return form.data.get(key, default) + + +def check_password_expired(user): + """ + Return True if password is expired, False otherwise. + """ + if not settings.ACCOUNT_PASSWORD_USE_HISTORY: + return False + + try: + # look for user-specific value + expiry = user.password_expiry.get() + except PasswordExpiry.DoesNotExist: + # use global value + expiry = settings.ACCOUNT_PASSWORD_EXPIRY + + try: + # get latest password info + latest = user.password_history.latest("timestamp") + except PasswordHistory.DoesNotExist: + return False + + if datetime.datetime.now() < (latest + datetime.timedelta(seconds=expiry)): + return False + + return True \ No newline at end of file diff --git a/account/views.py b/account/views.py index 5357806b..8298874b 100644 --- a/account/views.py +++ b/account/views.py @@ -1,17 +1,18 @@ from __future__ import unicode_literals +from django.core.urlresolvers import reverse from django.http import Http404, HttpResponseForbidden from django.shortcuts import redirect, get_object_or_404 from django.utils.http import base36_to_int, int_to_base36 -from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateResponseMixin, View from django.views.generic.edit import FormView from django.contrib import auth, messages from django.contrib.auth import get_user_model -from django.contrib.sites.shortcuts import get_current_site +from django.contrib.auth.hashers import make_password from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.shortcuts import get_current_site from account import signals from account.conf import settings @@ -20,11 +21,91 @@ from account.forms import SettingsForm from account.hooks import hookset from account.mixins import LoginRequiredMixin -from account.models import SignupCode, EmailAddress, EmailConfirmation, Account, AccountDeletion -from account.utils import default_redirect, get_form_data +from account.models import SignupCode, EmailAddress, EmailConfirmation, Account, AccountDeletion, PasswordHistory +from account.utils import check_password_expired, default_redirect, get_form_data + + +class PasswordMixin(object): + """ + Mixin handling common elements of password change. + + Required attributes in inheriting class: + + form_password_field - example: "password" + fallback_url_setting - example: "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" + + Required methods in inheriting class: + get_user() + change_password() + after_change_password() + get_redirect_field_name() -class SignupView(FormView): + """ + + redirect_field_name = "next" + messages = { + "password_changed": { + "level": messages.SUCCESS, + "text": _("Password successfully changed.") + } + } + + def get_context_data(self, **kwargs): + ctx = super(PasswordMixin, self).get_context_data(**kwargs) + redirect_field_name = self.get_redirect_field_name() + ctx.update({ + "redirect_field_name": redirect_field_name, + "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + }) + return ctx + + def change_password(self, form): + user = self.get_user() + user.set_password(form.cleaned_data[self.form_password_field]) + user.save() + return user + + def after_change_password(self): + user = self.get_user() + signals.password_changed.send(sender=self, user=user) + if settings.ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE: + self.send_password_email(user) + if self.messages.get("password_changed"): + messages.add_message( + self.request, + self.messages["password_changed"]["level"], + self.messages["password_changed"]["text"] + ) + + def get_redirect_field_name(self): + return self.redirect_field_name + + def get_success_url(self, fallback_url=None, **kwargs): + if fallback_url is None: + fallback_url = getattr(settings, self.fallback_url_setting, None) + kwargs.setdefault("redirect_field_name", self.get_redirect_field_name()) + return default_redirect(self.request, fallback_url, **kwargs) + + def send_password_email(self, user): + protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") + current_site = get_current_site(self.request) + ctx = { + "user": user, + "protocol": protocol, + "current_site": current_site, + } + hookset.send_password_change_email([user.email], ctx) + + def create_password_history(self, form): + if settings.ACCOUNT_PASSWORD_USE_HISTORY: + password = form.cleaned_data[self.form_password_field] + PasswordHistory.objects.create( + user=self.request.user, + password = make_password(password) + ) + +class SignupView(PasswordMixin, FormView): template_name = "account/signup.html" template_name_ajax = "account/ajax/signup.html" @@ -34,6 +115,7 @@ class SignupView(FormView): template_name_signup_closed_ajax = "account/ajax/signup_closed.html" form_class = SignupForm form_kwargs = {} + form_password_field = "password" redirect_field_name = "next" identifier_field = "username" messages = { @@ -46,6 +128,7 @@ class SignupView(FormView): "text": _("The code {code} is invalid.") } } + fallback_url_setting = "ACCOUNT_SIGNUP_REDIRECT_URL" def __init__(self, *args, **kwargs): self.created_user = None @@ -99,15 +182,6 @@ def get_template_names(self): else: return [self.template_name] - def get_context_data(self, **kwargs): - ctx = super(SignupView, self).get_context_data(**kwargs) - redirect_field_name = self.get_redirect_field_name() - ctx.update({ - "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), - }) - return ctx - def get_form_kwargs(self): kwargs = super(SignupView, self).get_form_kwargs() kwargs.update(self.form_kwargs) @@ -134,6 +208,7 @@ def form_valid(self, form): self.created_user.is_active = False self.created_user.save() self.create_account(form) + self.create_password_history(form) self.after_signup(form) if settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL and not email_address.verified: self.send_email_confirmation(email_address) @@ -160,15 +235,6 @@ def form_valid(self, form): self.login_user() return redirect(self.get_success_url()) - def get_success_url(self, fallback_url=None, **kwargs): - if fallback_url is None: - fallback_url = settings.ACCOUNT_SIGNUP_REDIRECT_URL - kwargs.setdefault("redirect_field_name", self.get_redirect_field_name()) - return default_redirect(self.request, fallback_url, **kwargs) - - def get_redirect_field_name(self): - return self.redirect_field_name - def create_user(self, form, commit=True, model=None, **kwargs): User = model if User is None: @@ -275,7 +341,6 @@ def closed(self): } return self.response_class(**response_kwargs) - class LoginView(FormView): template_name = "account/login.html" @@ -286,6 +351,11 @@ class LoginView(FormView): def get(self, *args, **kwargs): if self.request.user.is_authenticated(): + + # Check for password expiration, redirect if needed. + if check_password_expired(self.request.user): + return redirect("account_password") + return redirect(self.get_success_url()) return super(LoginView, self).get(*args, **kwargs) @@ -446,87 +516,7 @@ def after_confirmation(self, confirmation): user.save() -class ChangePasswordMixin(object): - """ - Mixin handling common elements of password change. - - Required attributes in inheriting class: - - form_password_field - example: "password" - fallback_url_setting - example: "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" - - Required methods in inheriting class: - - get_user() - change_password() - after_change_password() - get_redirect_field_name() - - """ - - redirect_field_name = "next" - messages = { - "password_changed": { - "level": messages.SUCCESS, - "text": _("Password successfully changed.") - } - } - - def get_context_data(self, **kwargs): - ctx = super(ChangePasswordMixin, self).get_context_data(**kwargs) - redirect_field_name = self.get_redirect_field_name() - ctx.update({ - "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), - }) - return ctx - - def change_password(self, form): - user = self.get_user() - user.set_password(form.cleaned_data[self.form_password_field]) - user.save() - # required on Django >= 1.7 to keep the user authenticated - if hasattr(auth, "update_session_auth_hash"): - auth.update_session_auth_hash(self.request, user) - - def after_change_password(self): - user = self.get_user() - signals.password_changed.send(sender=self, user=user) - if settings.ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE: - self.send_email(user) - if self.messages.get("password_changed"): - messages.add_message( - self.request, - self.messages["password_changed"]["level"], - self.messages["password_changed"]["text"] - ) - - def form_valid(self, form): - self.change_password(form) - self.after_change_password() - return redirect(self.get_success_url()) - - def get_redirect_field_name(self): - return self.redirect_field_name - - def get_success_url(self, fallback_url=None, **kwargs): - if fallback_url is None: - fallback_url = getattr(settings, self.fallback_url_setting, None) - kwargs.setdefault("redirect_field_name", self.get_redirect_field_name()) - return default_redirect(self.request, fallback_url, **kwargs) - - def send_email(self, user): - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") - current_site = get_current_site(self.request) - ctx = { - "user": user, - "protocol": protocol, - "current_site": current_site, - } - hookset.send_password_change_email([user.email], ctx) - - -class ChangePasswordView(ChangePasswordMixin, FormView): +class ChangePasswordView(PasswordMixin, FormView): template_name = "account/password_change.html" form_class = ChangePasswordForm @@ -550,6 +540,12 @@ def post(self, *args, **kwargs): return HttpResponseForbidden() return super(ChangePasswordView, self).post(*args, **kwargs) + def form_valid(self, form): + self.change_password(form) + self.create_password_history(form) + self.after_change_password() + return redirect(self.get_success_url()) + def get_user(self): return self.request.user @@ -565,6 +561,12 @@ def get_form_kwargs(self): }) return kwargs + def change_password(self, form): + user = super(ChangePasswordView, self).change_password(form) + # required on Django >= 1.7 to keep the user authenticated + if hasattr(auth, "update_session_auth_hash"): + auth.update_session_auth_hash(self.request, user) + class PasswordResetView(FormView): @@ -612,7 +614,7 @@ def make_token(self, user): return self.token_generator.make_token(user) -class PasswordResetTokenView(FormView): +class PasswordResetTokenView(PasswordMixin, FormView): template_name = "account/password_reset_token.html" template_name_fail = "account/password_reset_token_fail.html" @@ -637,6 +639,12 @@ def get_context_data(self, **kwargs): }) return ctx + def form_valid(self, form): + self.change_password(form) + self.create_password_history(form) + self.after_change_password() + return redirect(self.get_success_url()) + def get_user(self): try: uid_int = base36_to_int(self.kwargs["uidb36"]) diff --git a/docs/settings.rst b/docs/settings.rst index c530c14b..7cd7876e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -40,6 +40,16 @@ Default: ``"account_password"`` Default: ``"account_login"`` +``ACCOUNT_PASSWORD_EXPIRY`` +======================================= + +Default: ``0`` + +``ACCOUNT_PASSWORD_USE_HISTORY`` +======================================= + +Default: ``False`` + ``ACCOUNT_REMEMBER_ME_EXPIRY`` ============================== From 3635946b5611d21528508e696427ea20afe8dab3 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 1 Sep 2016 18:06:57 -0600 Subject: [PATCH 021/239] flake8 fixes --- account/utils.py | 2 +- account/views.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/account/utils.py b/account/utils.py index 1c26cae4..d84cdbd2 100644 --- a/account/utils.py +++ b/account/utils.py @@ -136,4 +136,4 @@ def check_password_expired(user): if datetime.datetime.now() < (latest + datetime.timedelta(seconds=expiry)): return False - return True \ No newline at end of file + return True diff --git a/account/views.py b/account/views.py index 8298874b..cc5539c6 100644 --- a/account/views.py +++ b/account/views.py @@ -102,9 +102,10 @@ def create_password_history(self, form): password = form.cleaned_data[self.form_password_field] PasswordHistory.objects.create( user=self.request.user, - password = make_password(password) + password=make_password(password) ) + class SignupView(PasswordMixin, FormView): template_name = "account/signup.html" @@ -341,6 +342,7 @@ def closed(self): } return self.response_class(**response_kwargs) + class LoginView(FormView): template_name = "account/login.html" From 7e53399f874419c5926c4b1f595492ce62d85a47 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Fri, 9 Sep 2016 09:23:25 -0600 Subject: [PATCH 022/239] Add password expiration tests Add makemigrations.py. --- account/tests/test_password.py | 86 ++++++++++++++++++++++++++++++++++ account/utils.py | 11 +++-- makemigrations.py | 49 +++++++++++++++++++ 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 account/tests/test_password.py create mode 100644 makemigrations.py diff --git a/account/tests/test_password.py b/account/tests/test_password.py new file mode 100644 index 00000000..a8879867 --- /dev/null +++ b/account/tests/test_password.py @@ -0,0 +1,86 @@ +import datetime + +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import ( + TestCase, + override_settings, +) + +from ..models import ( + PasswordExpiry, + PasswordHistory, +) + + +@override_settings( + AUTHENTICATION_BACKENDS=[ + "account.auth_backends.EmailAuthenticationBackend" + ], + ACCOUNT_PASSWORD_USE_HISTORY=True +) +class PasswordExpirationTestCase(TestCase): + + def setUp(self): + self.username = "user1" + self.email = "user1@example.com" + self.password = "changeme" + self.user = User.objects.create_user( + self.email, + email=self.email, + password=self.password, + ) + # create PasswordExpiry for user + self.expiry = PasswordExpiry.objects.create( + user=self.user, + expiry=60, # password expires after sixty seconds + ) + # create PasswordHistory for user + self.history = PasswordHistory.objects.create( + user=self.user, + password=make_password(self.password) + ) + + def test_signup(self): + """ + Ensure new user has one PasswordExpiry and one PasswordHistory. + """ + # PasswordExpiry.expiry should be same as ACCOUNT_PASSWORD_EXPIRY + pass + + def test_login_expired(self): + """ + Ensure user is redirected to change password if pw is expired. + """ + # set PasswordHistory timestamp in past so pw is expired. + self.history.timestamp = datetime.datetime.now() - datetime.timedelta(minutes=2) + self.history.save() + + # get login + u = self.client.login(username=self.email, password=self.password) + + response = self.client.get(reverse("account_login")) + self.assertRedirects(response, reverse("account_password")) + + # post login + post_data = { + "username": self.username, + "email": self.email, + "password": self.password, + } + response = self.client.post( + reverse("account_login"), + post_data + ) + self.assertEquals(response.status_code, 200) + + + def test_login_not_expired(self): + """ + Ensure user logs in successfully without redirect. + """ + # set PasswordHistory timestamp so pw is not expired. + # attempt login + # assert success and user logged in + pass diff --git a/account/utils.py b/account/utils.py index d84cdbd2..302274c2 100644 --- a/account/utils.py +++ b/account/utils.py @@ -2,6 +2,7 @@ import datetime import functools +import pytz try: from urllib.parse import urlparse, urlunparse except ImportError: # python 2 @@ -120,10 +121,10 @@ def check_password_expired(user): if not settings.ACCOUNT_PASSWORD_USE_HISTORY: return False - try: - # look for user-specific value - expiry = user.password_expiry.get() - except PasswordExpiry.DoesNotExist: + if hasattr(user, "password_expiry"): + # user-specific value + expiry = user.password_expiry.expiry + else: # use global value expiry = settings.ACCOUNT_PASSWORD_EXPIRY @@ -133,7 +134,7 @@ def check_password_expired(user): except PasswordHistory.DoesNotExist: return False - if datetime.datetime.now() < (latest + datetime.timedelta(seconds=expiry)): + if datetime.datetime.now(tz=pytz.UTC) > (latest.timestamp + datetime.timedelta(seconds=expiry)): return False return True diff --git a/makemigrations.py b/makemigrations.py new file mode 100644 index 00000000..56879415 --- /dev/null +++ b/makemigrations.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +import os +import sys + +import django + +from django.conf import settings + + +DEFAULT_SETTINGS = dict( + INSTALLED_APPS=[ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sites", + "account", + "account.tests" + ], + MIDDLEWARE_CLASSES=[], + DATABASES={ + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } + }, + SITE_ID=1, + ROOT_URLCONF="account.tests.urls", + SECRET_KEY="notasecret", +) + + +def run(*args): + if not settings.configured: + settings.configure(**DEFAULT_SETTINGS) + + django.setup() + + parent = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, parent) + + django.core.management.call_command( + "makemigrations", + "account", + *args + ) + + +if __name__ == "__main__": + run(*sys.argv[1:]) + From a7a58eb6b925c12e41f7a8515e32d930537d6357 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Fri, 9 Sep 2016 09:40:40 -0600 Subject: [PATCH 023/239] Fix flake8 complaints --- account/tests/test_password.py | 3 +-- account/utils.py | 5 +---- makemigrations.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/account/tests/test_password.py b/account/tests/test_password.py index a8879867..2cdbca57 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -58,7 +58,7 @@ def test_login_expired(self): self.history.save() # get login - u = self.client.login(username=self.email, password=self.password) + self.client.login(username=self.email, password=self.password) response = self.client.get(reverse("account_login")) self.assertRedirects(response, reverse("account_password")) @@ -75,7 +75,6 @@ def test_login_expired(self): ) self.assertEquals(response.status_code, 200) - def test_login_not_expired(self): """ Ensure user logs in successfully without redirect. diff --git a/account/utils.py b/account/utils.py index 302274c2..941ff87e 100644 --- a/account/utils.py +++ b/account/utils.py @@ -15,10 +15,7 @@ from django.contrib.auth import get_user_model from account.conf import settings -from .models import ( - PasswordHistory, - PasswordExpiry, -) +from .models import PasswordHistory def get_user_lookup_kwargs(kwargs): diff --git a/makemigrations.py b/makemigrations.py index 56879415..e6881231 100644 --- a/makemigrations.py +++ b/makemigrations.py @@ -46,4 +46,3 @@ def run(*args): if __name__ == "__main__": run(*sys.argv[1:]) - From 76ced621852ed5b041066f1697ed7aa6e12d1bf9 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Fri, 9 Sep 2016 11:03:07 -0600 Subject: [PATCH 024/239] Remove Python 3.2 from test matrix Add Django v1.10 to test matrix. --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 56b86cb6..e21bcd7f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,11 +6,10 @@ exclude = account/migrations/*,docs/* [tox] envlist = - py27-{1.8,1.9,master}, - py32-{1.8}, + py27-{1.8,1.9,1.10,master}, py33-{1.8}, - py34-{1.8,1.9,master}, - py35-{1.8,1.9,master} + py34-{1.8,1.9,1.10,master}, + py35-{1.8,1.9,1.10,master} [testenv] deps = @@ -19,6 +18,7 @@ deps = flake8==2.5.0 1.8: Django>=1.8,<1.9 1.9: Django>=1.9,<1.10 + 1.10: Django>=1.10,<1.11 master: https://github.com/django/django/tarball/master usedevelop = True setenv = From 9c3f71c9145984bb0a75e693c853afbe9a169287 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Sep 2016 11:18:18 -0600 Subject: [PATCH 025/239] Removed deprecated ACCOUNT_USE_AUTH_AUTHENTICATE --- CHANGELOG.md | 1 + account/conf.py | 1 - account/views.py | 11 +---------- docs/faq.rst | 28 ---------------------------- docs/settings.rst | 5 ----- 5 files changed, 2 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60198cd6..5879c937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ version with these. Your code will need to be updated to continue working. * BI: account deletion callbacks moved to hooksets * BI: dropped Django 1.7 support + * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value * added Turkish translations * fixed migration with language codes to dynamically set diff --git a/account/conf.py b/account/conf.py index 0efa0d7b..352b1f60 100644 --- a/account/conf.py +++ b/account/conf.py @@ -55,7 +55,6 @@ class AccountAppConf(AppConf): HOOKSET = "account.hooks.AccountDefaultHookSet" TIMEZONES = TIMEZONES LANGUAGES = LANGUAGES - USE_AUTH_AUTHENTICATE = False def configure_hookset(self, value): return load_path_attr(value)() diff --git a/account/views.py b/account/views.py index cc5539c6..f3f1bafb 100644 --- a/account/views.py +++ b/account/views.py @@ -282,16 +282,7 @@ def after_signup(self, form): signals.user_signed_up.send(sender=SignupForm, user=self.created_user, form=form) def login_user(self): - user = self.created_user - if settings.ACCOUNT_USE_AUTH_AUTHENTICATE: - # call auth.authenticate to ensure we set the correct backend for - # future look ups using auth.get_user(). - user = auth.authenticate(**self.user_credentials()) - else: - # set auth backend to ModelBackend, but this may not be used by - # everyone. this code path is deprecated and will be removed in - # favor of using auth.authenticate above. - user.backend = "django.contrib.auth.backends.ModelBackend" + user = auth.authenticate(**self.user_credentials()) auth.login(self.request, user) self.request.session.set_expiry(0) diff --git a/docs/faq.rst b/docs/faq.rst index 004dca2b..8e9a659d 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -37,31 +37,3 @@ because you can choose not to do any email address storage. If you don't use a custom user model then make sure you take extra precaution. When editing email addresses either in the shell or admin make sure you update in both places. Only the primary email address is stored on the ``User`` model. - -Why does auto-login after sign up not log my user in? -===================================================== - -If you are using Django 1.6+ and ``django.contrib.auth.backends.ModelBackend`` -does not exist in your ``AUTHENTICATION_BACKENDS`` then you will experience an -issue where users are not logged in after sign up (when -``ACCOUNT_EMAIL_CONFIRMATION_REQUIRED`` is ``False``.) - -This has been fixed, but the default behavior is buggy (for this use case) to -maintain backward compatibility. In a future version of django-user-accounts -the default behavior will not be buggy. - -To fix, simply set:: - - ACCOUNT_USE_AUTH_AUTHENTICATE = True - -in your Django settings. This will cause the ``login_user`` method of -``SignupView`` to use proper backend authentication to determine the correct -authentication backend for the user. You will need to make sure that -``SignupView.identifier_field`` is set to represent the correct field on the -sign up form to use as the username for credentials. By default the ``username`` -field is used (to be consistent with the default username authentication used -for log in.) - -If you have a custom need for user credentials passed to the authentication -backends, you may override the behavior using the hookset -``get_user_credentials``. diff --git a/docs/settings.rst b/docs/settings.rst index 7cd7876e..6cb76db3 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -148,8 +148,3 @@ Default: ``list(zip(pytz.all_timezones, pytz.all_timezones))`` ===================== See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/language_list.py - -``ACCOUNT_USE_AUTH_AUTHENTICATE`` -================================= - -Default: ``False`` From d4d09397b7eaea46a36296fb3d23bbbbf9de997a Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Sat, 10 Sep 2016 13:49:27 -0600 Subject: [PATCH 026/239] Expand tests, fix logic, add docs Add mgmt command for setting user-specific password expiry. Add documentation for password expiry. --- README.rst | 7 +- .../commands/user_password_expiry.py | 31 ++++++++ account/migrations/0004_auto_20160909_1235.py | 20 +++++ account/models.py | 2 +- account/tests/test_password.py | 78 +++++++++++++------ account/utils.py | 12 ++- account/views.py | 10 +-- docs/changelog.rst | 7 ++ docs/usage.rst | 22 ++++++ 9 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 account/management/commands/user_password_expiry.py create mode 100644 account/migrations/0004_auto_20160909_1235.py diff --git a/README.rst b/README.rst index 70825cb8..ad2f4333 100644 --- a/README.rst +++ b/README.rst @@ -47,6 +47,7 @@ Features - Email confirmation - Signup tokens for private betas - Password reset + - Password expiration - Account management (update account settings and change password) - Account deletion @@ -57,7 +58,7 @@ Features Requirements -------------- -* Django 1.8 or 1.9 +* Django 1.8, 1.9, or 1.10 * Python 2.7, 3.3, 3.4 or 3.5 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) @@ -79,13 +80,13 @@ See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax- In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. -We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). +We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). Code of Conduct ----------------- -In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. +In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. diff --git a/account/management/commands/user_password_expiry.py b/account/management/commands/user_password_expiry.py new file mode 100644 index 00000000..6766284f --- /dev/null +++ b/account/management/commands/user_password_expiry.py @@ -0,0 +1,31 @@ +from django.core.management.base import LabelCommand + +from account.conf import settings +from account.models import PasswordExpiry + + +class Command(LabelCommand): + + help = "Create user-specific password expiration." + label = "username" + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument("-e", "--expire", default=settings.ACCOUNT_PASSWORD_EXPIRY) + + def handle_label(self, username, **options): + try: + user = settings.AUTH_USER_MODEL.objects.get(username=username) + except settings.AUTH_USER_MODEL.DoesNotExist: + return "User \"{}\" not found".format(username) + + expire = options["expire"] + + # Modify existing PasswordExpiry or create new if needed. + if not hasattr(user, "password_expiry"): + PasswordExpiry.objects.create(user=user, expiry=expire) + else: + user.password_expiry.expiry = expire + user.password_expiry.save() + + return "User \"{}\" password expiration now {} seconds".format(username, expire) diff --git a/account/migrations/0004_auto_20160909_1235.py b/account/migrations/0004_auto_20160909_1235.py new file mode 100644 index 00000000..8f32d77e --- /dev/null +++ b/account/migrations/0004_auto_20160909_1235.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.1 on 2016-09-09 12:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_passwordexpiry_passwordhistory'), + ] + + operations = [ + migrations.AlterField( + model_name='passwordhistory', + name='timestamp', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/account/models.py b/account/models.py index 047609b7..e0beff3e 100644 --- a/account/models.py +++ b/account/models.py @@ -398,7 +398,7 @@ class PasswordHistory(models.Model): """ user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history") password = models.CharField(max_length=255) # encrypted password - timestamp = models.DateTimeField(auto_now=True) + timestamp = models.DateTimeField(auto_now_add=True) class PasswordExpiry(models.Model): diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 2cdbca57..24b3fd1d 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -1,4 +1,5 @@ import datetime +import pytz from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User @@ -12,12 +13,10 @@ PasswordExpiry, PasswordHistory, ) +from ..utils import check_password_expired @override_settings( - AUTHENTICATION_BACKENDS=[ - "account.auth_backends.EmailAuthenticationBackend" - ], ACCOUNT_PASSWORD_USE_HISTORY=True ) class PasswordExpirationTestCase(TestCase): @@ -27,7 +26,7 @@ def setUp(self): self.email = "user1@example.com" self.password = "changeme" self.user = User.objects.create_user( - self.email, + self.username, email=self.email, password=self.password, ) @@ -44,42 +43,73 @@ def setUp(self): def test_signup(self): """ - Ensure new user has one PasswordExpiry and one PasswordHistory. + Ensure new user has one PasswordHistory and no PasswordExpiry. """ - # PasswordExpiry.expiry should be same as ACCOUNT_PASSWORD_EXPIRY - pass + email = "foobar@example.com" + post_data = { + "username": "foo", + "password": "bar", + "password_confirm": "bar", + "email": email, + } + response = self.client.post(reverse("account_signup"), post_data) + self.assertEqual(response.status_code, 302) + user = User.objects.get(email=email) + self.assertFalse(hasattr(user, "password_expiry")) + self.assertTrue(hasattr(user, "password_history")) + + # verify password is not expired + self.assertFalse(check_password_expired(user)) + + def test_login_not_expired(self): + """ + Ensure user can log in successfully without redirect. + """ + # get login + self.client.login(username=self.username, password=self.password) + + response = self.client.get(reverse("account_login")) + self.assertRedirects(response, "/", fetch_redirect_response=False) def test_login_expired(self): """ Ensure user is redirected to change password if pw is expired. """ # set PasswordHistory timestamp in past so pw is expired. - self.history.timestamp = datetime.datetime.now() - datetime.timedelta(minutes=2) + self.history.timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) self.history.save() # get login - self.client.login(username=self.email, password=self.password) + self.client.login(username=self.username, password=self.password) response = self.client.get(reverse("account_login")) self.assertRedirects(response, reverse("account_password")) - # post login + def test_pw_expiration_reset(self): + """ + Ensure changing password results in new PasswordHistory. + """ + qs = PasswordHistory.objects.all() + self.assertEquals(qs.count(), 1) + + # get login + self.client.login(username=self.username, password=self.password) + + # post new password to reset PasswordHistory + new_password = "lynyrdskynyrd" post_data = { - "username": self.username, - "email": self.email, - "password": self.password, + "password_current": self.password, + "password_new": new_password, + "password_new_confirm": new_password, } - response = self.client.post( - reverse("account_login"), + self.client.post( + reverse("account_password"), post_data ) - self.assertEquals(response.status_code, 200) - def test_login_not_expired(self): - """ - Ensure user logs in successfully without redirect. - """ - # set PasswordHistory timestamp so pw is not expired. - # attempt login - # assert success and user logged in - pass + qs = PasswordHistory.objects.all() + self.assertEquals(qs.count(), 2) + + latest = PasswordHistory.objects.latest("timestamp") + self.assertTrue(latest != self.history) + self.assertTrue(latest.timestamp > self.history.timestamp) diff --git a/account/utils.py b/account/utils.py index 941ff87e..783f6bcb 100644 --- a/account/utils.py +++ b/account/utils.py @@ -125,13 +125,19 @@ def check_password_expired(user): # use global value expiry = settings.ACCOUNT_PASSWORD_EXPIRY + if expiry == 0: # zero indicates no expiration + return False + try: # get latest password info latest = user.password_history.latest("timestamp") except PasswordHistory.DoesNotExist: return False - if datetime.datetime.now(tz=pytz.UTC) > (latest.timestamp + datetime.timedelta(seconds=expiry)): - return False + now = datetime.datetime.now(tz=pytz.UTC) + expiration = latest.timestamp + datetime.timedelta(seconds=expiry) - return True + if expiration < now: + return True + else: + return False diff --git a/account/views.py b/account/views.py index f3f1bafb..9d3e7022 100644 --- a/account/views.py +++ b/account/views.py @@ -97,11 +97,11 @@ def send_password_email(self, user): } hookset.send_password_change_email([user.email], ctx) - def create_password_history(self, form): + def create_password_history(self, form, user): if settings.ACCOUNT_PASSWORD_USE_HISTORY: password = form.cleaned_data[self.form_password_field] PasswordHistory.objects.create( - user=self.request.user, + user=user, password=make_password(password) ) @@ -209,7 +209,7 @@ def form_valid(self, form): self.created_user.is_active = False self.created_user.save() self.create_account(form) - self.create_password_history(form) + self.create_password_history(form, self.created_user) self.after_signup(form) if settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL and not email_address.verified: self.send_email_confirmation(email_address) @@ -535,7 +535,7 @@ def post(self, *args, **kwargs): def form_valid(self, form): self.change_password(form) - self.create_password_history(form) + self.create_password_history(form, self.request.user) self.after_change_password() return redirect(self.get_success_url()) @@ -634,7 +634,7 @@ def get_context_data(self, **kwargs): def form_valid(self, form): self.change_password(form) - self.create_password_history(form) + self.create_password_history(form, self.request.user) self.after_change_password() return redirect(self.get_success_url()) diff --git a/docs/changelog.rst b/docs/changelog.rst index ea0cad9a..1ed54927 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,13 @@ CHANGELOG ========= +2.0 +--- + +* add password expiration +* add Django v1.10 to test compatibility matrix +* remove Python 3.2 from text compatibility matrix + 1.0 --- diff --git a/docs/usage.rst b/docs/usage.rst index e1b19a1a..1e52d17d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -254,3 +254,25 @@ file called lib/tests.py:: And in your settings:: TEST_RUNNER = "lib.tests.MyTestDiscoverRunner" + + +Enabling password expiration +============================ + +Password expiration is disabled by default. In order to enable password expiration +you must add two entries to your settings file:: + + PASSWORD_EXPIRY = 60*60*24*5 # seconds until pw expires, this example shows five days + PASSWORD_USE_HISTORY = True + +PASSWORD_EXPIRY indicates the duration a password will stay valid. After that period +the password must be reset in order to log in. If PASSWORD_EXPIRY is zero (0) +then passwords never expire. + +If PASSWORD_USE_HISTORY is False, no history will be generated and password +expiration WILL NOT be checked. + +If PASSWORD_USE_HISTORY is True, a password history entry is created each time +the user changes their password. This entry links the user with their most recent +(encrypted) password and a timestamp. Unless deleted manually, PasswordHistory items +are saved forever, allowing password history checking for new passwords. From 23edd12e8d690084d780a13858485eea1c7ad2a5 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 09:02:35 -0600 Subject: [PATCH 027/239] Add mgmt commands and tests Revise model migration. Add user_password_history command. Add management command tests. Improve password tests. Add Django admin functionality for new PasswordExpiry and PasswordHistory models. --- account/admin.py | 21 +- .../commands/user_password_expiry.py | 18 +- .../commands/user_password_history.py | 45 ++++ .../0003_passwordexpiry_passwordhistory.py | 5 +- account/migrations/0004_auto_20160909_1235.py | 20 -- account/models.py | 4 +- account/tests/test_commands.py | 227 ++++++++++++++++++ account/tests/test_password.py | 108 ++++++++- 8 files changed, 407 insertions(+), 41 deletions(-) create mode 100644 account/management/commands/user_password_history.py delete mode 100644 account/migrations/0004_auto_20160909_1235.py create mode 100644 account/tests/test_commands.py diff --git a/account/admin.py b/account/admin.py index 7e0e3a28..3f01f5ce 100644 --- a/account/admin.py +++ b/account/admin.py @@ -2,7 +2,14 @@ from django.contrib import admin -from account.models import Account, SignupCode, AccountDeletion, EmailAddress +from account.models import ( + Account, + AccountDeletion, + EmailAddress, + PasswordExpiry, + PasswordHistory, + SignupCode, +) class SignupCodeAdmin(admin.ModelAdmin): @@ -29,7 +36,19 @@ class EmailAddressAdmin(AccountAdmin): search_fields = ["email", "user__username"] +class PasswordExpiryAdmin(admin.ModelAdmin): + + raw_id_fields = ["user"] + + +class PasswordHistoryAdmin(admin.ModelAdmin): + + raw_id_fields = ["user"] + + admin.site.register(Account, AccountAdmin) admin.site.register(SignupCode, SignupCodeAdmin) admin.site.register(AccountDeletion, AccountDeletionAdmin) admin.site.register(EmailAddress, EmailAddressAdmin) +admin.site.register(PasswordExpiry, PasswordExpiryAdmin) +admin.site.register(PasswordHistory, PasswordHistoryAdmin) diff --git a/account/management/commands/user_password_expiry.py b/account/management/commands/user_password_expiry.py index 6766284f..b13424c8 100644 --- a/account/management/commands/user_password_expiry.py +++ b/account/management/commands/user_password_expiry.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.core.management.base import LabelCommand from account.conf import settings @@ -6,17 +7,24 @@ class Command(LabelCommand): - help = "Create user-specific password expiration." + help = "Create user-specific password expiration period." label = "username" def add_arguments(self, parser): super(Command, self).add_arguments(parser) - parser.add_argument("-e", "--expire", default=settings.ACCOUNT_PASSWORD_EXPIRY) + parser.add_argument( + "-e", "--expire", + type=int, + nargs="?", + default=settings.ACCOUNT_PASSWORD_EXPIRY, + help="number of seconds until password expires" + ) def handle_label(self, username, **options): + User = get_user_model() try: - user = settings.AUTH_USER_MODEL.objects.get(username=username) - except settings.AUTH_USER_MODEL.DoesNotExist: + user = User.objects.get(username=username) + except User.DoesNotExist: return "User \"{}\" not found".format(username) expire = options["expire"] @@ -28,4 +36,4 @@ def handle_label(self, username, **options): user.password_expiry.expiry = expire user.password_expiry.save() - return "User \"{}\" password expiration now {} seconds".format(username, expire) + return "User \"{}\" password expiration set to {} seconds".format(username, expire) diff --git a/account/management/commands/user_password_history.py b/account/management/commands/user_password_history.py new file mode 100644 index 00000000..4349416f --- /dev/null +++ b/account/management/commands/user_password_history.py @@ -0,0 +1,45 @@ +import datetime +import pytz + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + +from account.models import PasswordHistory + + +class Command(BaseCommand): + + help = "Create password history for all users without existing history." + + def add_arguments(self, parser): + parser.add_argument( + "-d", "--days", + type=int, + nargs="?", + default=10, + help="age of current password (in days)" + ) + parser.add_argument( + "-f", "--force", + action="store_true", + help="create new password history for all users, regardless of existing history" + ) + + def handle(self, *args, **options): + User = get_user_model() + users = User.objects.all() + if not options["force"]: + users = users.filter(password_history=None) + + if not users: + return "No users found without password history" + + days = options["days"] + timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=days) + + # Create new PasswordHistory on `timestamp` + PasswordHistory.objects.bulk_create( + [PasswordHistory(user=user, timestamp=timestamp) for user in users] + ) + + return "Password history set to {} for {} users".format(timestamp, len(users)) diff --git a/account/migrations/0003_passwordexpiry_passwordhistory.py b/account/migrations/0003_passwordexpiry_passwordhistory.py index 359017e9..2c71d0c3 100644 --- a/account/migrations/0003_passwordexpiry_passwordhistory.py +++ b/account/migrations/0003_passwordexpiry_passwordhistory.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.7 on 2016-09-01 17:50 +# Generated by Django 1.10.1 on 2016-09-13 08:55 from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): @@ -28,7 +29,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=255)), - ('timestamp', models.DateTimeField(auto_now=True)), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='password_history', to=settings.AUTH_USER_MODEL)), ], ), diff --git a/account/migrations/0004_auto_20160909_1235.py b/account/migrations/0004_auto_20160909_1235.py deleted file mode 100644 index 8f32d77e..00000000 --- a/account/migrations/0004_auto_20160909_1235.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.1 on 2016-09-09 12:35 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0003_passwordexpiry_passwordhistory'), - ] - - operations = [ - migrations.AlterField( - model_name='passwordhistory', - name='timestamp', - field=models.DateTimeField(auto_now_add=True), - ), - ] diff --git a/account/models.py b/account/models.py index e0beff3e..345f0d47 100644 --- a/account/models.py +++ b/account/models.py @@ -104,7 +104,7 @@ def user_post_save(sender, **kwargs): """ # Disable post_save during manage.py loaddata - if kwargs.get('raw', False): + if kwargs.get("raw", False): return False user, created = kwargs["instance"], kwargs["created"] @@ -398,7 +398,7 @@ class PasswordHistory(models.Model): """ user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history") password = models.CharField(max_length=255) # encrypted password - timestamp = models.DateTimeField(auto_now_add=True) + timestamp = models.DateTimeField(default=timezone.now) # password creation time class PasswordExpiry(models.Model): diff --git a/account/tests/test_commands.py b/account/tests/test_commands.py new file mode 100644 index 00000000..594c9e71 --- /dev/null +++ b/account/tests/test_commands.py @@ -0,0 +1,227 @@ +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.test import ( + override_settings, + TestCase, +) +from django.utils.six import StringIO + +from ..conf import settings +from ..models import ( + PasswordExpiry, + PasswordHistory, +) + + +@override_settings( + ACCOUNT_PASSWORD_EXPIRY=500 +) +class UserPasswordExpiryTests(TestCase): + + def setUp(self): + self.UserModel = get_user_model() + self.user = self.UserModel.objects.create_user(username="patrick") + + def test_set_explicit_password_expiry(self): + """ + Ensure specific password expiry is set. + """ + self.assertFalse(hasattr(self.user, "password_expiry")) + expiration_period = 60 + out = StringIO() + call_command( + "user_password_expiry", + "patrick", + "--expire={}".format(expiration_period), + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + user_expiry = user.password_expiry + self.assertEqual(user_expiry.expiry, expiration_period) + self.assertIn("User \"{}\" password expiration set to {} seconds".format(self.user.username, expiration_period), out.getvalue()) + + def test_set_default_password_expiry(self): + """ + Ensure default password expiry (from settings) is set. + """ + self.assertFalse(hasattr(self.user, "password_expiry")) + out = StringIO() + call_command( + "user_password_expiry", + "patrick", + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + user_expiry = user.password_expiry + default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY + self.assertEqual(user_expiry.expiry, default_expiration) + self.assertIn("User \"{}\" password expiration set to {} seconds".format(self.user.username, default_expiration), out.getvalue()) + + def test_reset_existing_password_expiry(self): + """ + Ensure existing password expiry is reset. + """ + previous_expiry = 123 + existing_expiry = PasswordExpiry.objects.create(user=self.user, expiry=previous_expiry) + out = StringIO() + call_command( + "user_password_expiry", + "patrick", + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + user_expiry = user.password_expiry + self.assertEqual(user_expiry, existing_expiry) + default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY + self.assertEqual(user_expiry.expiry, default_expiration) + self.assertNotEqual(user_expiry.expiry, previous_expiry) + + def test_bad_username(self): + """ + Ensure proper operation when username is not found. + """ + bad_username = "asldkfj" + out = StringIO() + call_command( + "user_password_expiry", + bad_username, + stdout=out + ) + self.assertIn("User \"{}\" not found".format(bad_username), out.getvalue()) + + +class UserPasswordHistoryTests(TestCase): + + def setUp(self): + self.UserModel = get_user_model() + self.user = self.UserModel.objects.create_user(username="patrick") + + def test_set_history(self): + """ + Ensure password history is created. + """ + self.assertFalse(self.user.password_history.all()) + password_age = 5 # days + out = StringIO() + call_command( + "user_password_history", + "--days={}".format(password_age), + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + self.assertIn("Password history set to ", out.getvalue()) + self.assertIn("for {} users".format(1), out.getvalue()) + + def test_set_history_exists(self): + """ + Ensure password history is NOT created. + """ + PasswordHistory.objects.create(user=self.user) + password_age = 5 # days + out = StringIO() + call_command( + "user_password_history", + "--days={}".format(password_age), + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + self.assertIn("No users found without password history", out.getvalue()) + + def test_set_history_one_exists(self): + """ + Ensure password history is created for users without existing history. + """ + another_user = self.UserModel.objects.create_user(username="james") + PasswordHistory.objects.create(user=another_user) + + password_age = 5 # days + out = StringIO() + call_command( + "user_password_history", + "--days={}".format(password_age), + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + + # verify user with existing history did not get another entry + user = self.UserModel.objects.get(username="james") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + + self.assertIn("Password history set to ", out.getvalue()) + self.assertIn("for {} users".format(1), out.getvalue()) + + def test_set_history_force(self): + """ + Ensure specific password history is created for all users. + """ + another_user = self.UserModel.objects.create_user(username="james") + PasswordHistory.objects.create(user=another_user) + + password_age = 5 # days + out = StringIO() + call_command( + "user_password_history", + "--days={}".format(password_age), + "--force", + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + + # verify user with existing history DID get another entry + user = self.UserModel.objects.get(username="james") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 2) + + self.assertIn("Password history set to ", out.getvalue()) + self.assertIn("for {} users".format(2), out.getvalue()) + + def test_set_history_multiple(self): + """ + Ensure password history is created for all users without existing history. + """ + self.UserModel.objects.create_user(username="second") + self.UserModel.objects.create_user(username="third") + + password_age = 5 # days + out = StringIO() + call_command( + "user_password_history", + "--days={}".format(password_age), + stdout=out + ) + + user = self.UserModel.objects.get(username="patrick") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + first_timestamp = password_history[0].timestamp + + user = self.UserModel.objects.get(username="second") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + second_timestamp = password_history[0].timestamp + self.assertEqual(first_timestamp, second_timestamp) + + user = self.UserModel.objects.get(username="third") + password_history = user.password_history.all() + self.assertEqual(password_history.count(), 1) + third_timestamp = password_history[0].timestamp + self.assertEqual(first_timestamp, third_timestamp) + + self.assertIn("Password history set to ", out.getvalue()) + self.assertIn("for {} users".format(3), out.getvalue()) diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 24b3fd1d..5cef7ba8 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -1,7 +1,10 @@ import datetime import pytz -from django.contrib.auth.hashers import make_password +from django.contrib.auth.hashers import ( + check_password, + make_password, +) from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test import ( @@ -46,28 +49,32 @@ def test_signup(self): Ensure new user has one PasswordHistory and no PasswordExpiry. """ email = "foobar@example.com" + password = "bar" post_data = { "username": "foo", - "password": "bar", - "password_confirm": "bar", + "password": password, + "password_confirm": password, "email": email, } response = self.client.post(reverse("account_signup"), post_data) self.assertEqual(response.status_code, 302) user = User.objects.get(email=email) self.assertFalse(hasattr(user, "password_expiry")) - self.assertTrue(hasattr(user, "password_history")) + latest_history = user.password_history.latest("timestamp") + self.assertTrue(latest_history) # verify password is not expired self.assertFalse(check_password_expired(user)) + # verify raw password matches encrypted password in history + self.assertTrue(check_password(password, latest_history.password)) def test_login_not_expired(self): """ Ensure user can log in successfully without redirect. """ - # get login self.client.login(username=self.username, password=self.password) + # get login response = self.client.get(reverse("account_login")) self.assertRedirects(response, "/", fetch_redirect_response=False) @@ -79,9 +86,9 @@ def test_login_expired(self): self.history.timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) self.history.save() - # get login self.client.login(username=self.username, password=self.password) + # get login response = self.client.get(reverse("account_login")) self.assertRedirects(response, reverse("account_password")) @@ -89,8 +96,7 @@ def test_pw_expiration_reset(self): """ Ensure changing password results in new PasswordHistory. """ - qs = PasswordHistory.objects.all() - self.assertEquals(qs.count(), 1) + history_count = self.user.password_history.count() # get login self.client.login(username=self.username, password=self.password) @@ -106,10 +112,90 @@ def test_pw_expiration_reset(self): reverse("account_password"), post_data ) - - qs = PasswordHistory.objects.all() - self.assertEquals(qs.count(), 2) + # Should see one more history entry for this user + self.assertEquals(self.user.password_history.count(), history_count + 1) latest = PasswordHistory.objects.latest("timestamp") self.assertTrue(latest != self.history) self.assertTrue(latest.timestamp > self.history.timestamp) + + +class ExistingUserNoHistoryTestCase(TestCase): + """ + Tests where user has no PasswordHistory. + """ + + def setUp(self): + self.username = "user1" + self.email = "user1@example.com" + self.password = "changeme" + self.user = User.objects.create_user( + self.username, + email=self.email, + password=self.password, + ) + + @override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=True + ) + def test_login_not_expired(self): + """ + Ensure user without history can log in successfully without redirect. + """ + self.client.login(username=self.username, password=self.password) + + # get login + response = self.client.get(reverse("account_login")) + self.assertRedirects(response, "/", fetch_redirect_response=False) + + @override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=True + ) + def test_pw_expiration_reset(self): + """ + Ensure changing password results in new PasswordHistory, + even when no PasswordHistory exists. + """ + history_count = self.user.password_history.count() + + # get login + self.client.login(username=self.username, password=self.password) + + # post new password to reset PasswordHistory + new_password = "lynyrdskynyrd" + post_data = { + "password_current": self.password, + "password_new": new_password, + "password_new_confirm": new_password, + } + self.client.post( + reverse("account_password"), + post_data + ) + # Should see one more history entry for this user + self.assertEquals(self.user.password_history.count(), history_count + 1) + + @override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=False + ) + def test_password_reset(self): + """ + Ensure changing password results in NO new PasswordHistory + when ACCOUNT_PASSWORD_USE_HISTORY == False. + """ + # get login + self.client.login(username=self.username, password=self.password) + + # post new password to reset PasswordHistory + new_password = "lynyrdskynyrd" + post_data = { + "password_current": self.password, + "password_new": new_password, + "password_new_confirm": new_password, + } + self.client.post( + reverse("account_password"), + post_data + ) + # history count should be zero + self.assertEquals(self.user.password_history.count(), 0) From c9c73d881ed0a1cb478ad68e789875e530d8debc Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 12:33:13 -0600 Subject: [PATCH 028/239] Add ExpiredPasswordMiddleware Use middleware to detect password expiration and redirect to password change page if expired. Added to CHANGELOG.md. Removed outdated docs/changelog.rst. Added documentation for password middleware. --- CHANGELOG.md | 5 +- account/middleware.py | 16 ++++ account/tests/templates/account/settings.html | 1 + account/tests/test_password.py | 90 +++++++++++-------- account/views.py | 7 +- docs/changelog.rst | 17 ---- docs/index.rst | 1 - docs/usage.rst | 16 +++- 8 files changed, 87 insertions(+), 66 deletions(-) create mode 100644 account/tests/templates/account/settings.html delete mode 100644 docs/changelog.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 5879c937..b4170750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,12 @@ version with these. Your code will need to be updated to continue working. * BI: account deletion callbacks moved to hooksets * BI: dropped Django 1.7 support - * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value + * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value + * BI: remove Python 3.2 from test compatibility matrix + * add Django v1.10 to test compatibility matrix * added Turkish translations * fixed migration with language codes to dynamically set + * add password expiration ## 1.3.0 diff --git a/account/middleware.py b/account/middleware.py index 916eaf19..5118af63 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,10 +1,13 @@ from __future__ import unicode_literals +from django.core.urlresolvers import resolve +from django.shortcuts import redirect from django.utils import translation, timezone from django.utils.cache import patch_vary_headers from account.conf import settings from account.models import Account +from account.utils import check_password_expired class LocaleMiddleware(object): @@ -51,3 +54,16 @@ def process_request(self, request): if account: tz = settings.TIME_ZONE if not account.timezone else account.timezone timezone.activate(tz) + + +class ExpiredPasswordMiddleware(object): + + def process_request(self, request): + if request.user.is_authenticated() and not request.user.is_staff: + url_name = resolve(request.path).url_name + # All users must be allowed to access "change password" url. + if url_name not in settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL: + if check_password_expired(request.user): + return redirect( + settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL + ) diff --git a/account/tests/templates/account/settings.html b/account/tests/templates/account/settings.html new file mode 100644 index 00000000..9ed72bb4 --- /dev/null +++ b/account/tests/templates/account/settings.html @@ -0,0 +1 @@ +# empty for now diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 5cef7ba8..42b19f03 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from django.test import ( TestCase, + modify_settings, override_settings, ) @@ -22,6 +23,11 @@ @override_settings( ACCOUNT_PASSWORD_USE_HISTORY=True ) +@modify_settings( + MIDDLEWARE_CLASSES={ + 'append': 'account.middleware.ExpiredPasswordMiddleware' + } +) class PasswordExpirationTestCase(TestCase): def setUp(self): @@ -68,31 +74,33 @@ def test_signup(self): # verify raw password matches encrypted password in history self.assertTrue(check_password(password, latest_history.password)) - def test_login_not_expired(self): + def test_get_not_expired(self): """ - Ensure user can log in successfully without redirect. + Ensure authenticated user can retrieve account settings page + without "password change" redirect. """ self.client.login(username=self.username, password=self.password) - # get login - response = self.client.get(reverse("account_login")) - self.assertRedirects(response, "/", fetch_redirect_response=False) + # get account settings page + response = self.client.get(reverse("account_settings")) + self.assertEquals(response.status_code, 200) - def test_login_expired(self): + def test_get_expired(self): """ - Ensure user is redirected to change password if pw is expired. + Ensure authenticated user is redirected to change password + when retrieving account settings page if password is expired. """ - # set PasswordHistory timestamp in past so pw is expired. + # set PasswordHistory timestamp in past so password is expired. self.history.timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) self.history.save() self.client.login(username=self.username, password=self.password) - # get login - response = self.client.get(reverse("account_login")) + # get account settings page + response = self.client.get(reverse("account_settings")) self.assertRedirects(response, reverse("account_password")) - def test_pw_expiration_reset(self): + def test_password_expiration_reset(self): """ Ensure changing password results in new PasswordHistory. """ @@ -120,6 +128,11 @@ def test_pw_expiration_reset(self): self.assertTrue(latest.timestamp > self.history.timestamp) +@modify_settings( + MIDDLEWARE_CLASSES={ + 'append': 'account.middleware.ExpiredPasswordMiddleware' + } +) class ExistingUserNoHistoryTestCase(TestCase): """ Tests where user has no PasswordHistory. @@ -135,23 +148,21 @@ def setUp(self): password=self.password, ) - @override_settings( - ACCOUNT_PASSWORD_USE_HISTORY=True - ) - def test_login_not_expired(self): + def test_get_no_history(self): """ - Ensure user without history can log in successfully without redirect. + Ensure authenticated user without password history can retrieve + account settings page without "password change" redirect. """ self.client.login(username=self.username, password=self.password) - # get login - response = self.client.get(reverse("account_login")) - self.assertRedirects(response, "/", fetch_redirect_response=False) + with override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=True + ): + # get account settings page + response = self.client.get(reverse("account_settings")) + self.assertEquals(response.status_code, 200) - @override_settings( - ACCOUNT_PASSWORD_USE_HISTORY=True - ) - def test_pw_expiration_reset(self): + def test_password_expiration_reset(self): """ Ensure changing password results in new PasswordHistory, even when no PasswordHistory exists. @@ -168,16 +179,16 @@ def test_pw_expiration_reset(self): "password_new": new_password, "password_new_confirm": new_password, } - self.client.post( - reverse("account_password"), - post_data - ) - # Should see one more history entry for this user - self.assertEquals(self.user.password_history.count(), history_count + 1) + with override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=True + ): + self.client.post( + reverse("account_password"), + post_data + ) + # Should see one more history entry for this user + self.assertEquals(self.user.password_history.count(), history_count + 1) - @override_settings( - ACCOUNT_PASSWORD_USE_HISTORY=False - ) def test_password_reset(self): """ Ensure changing password results in NO new PasswordHistory @@ -193,9 +204,12 @@ def test_password_reset(self): "password_new": new_password, "password_new_confirm": new_password, } - self.client.post( - reverse("account_password"), - post_data - ) - # history count should be zero - self.assertEquals(self.user.password_history.count(), 0) + with override_settings( + ACCOUNT_PASSWORD_USE_HISTORY=False + ): + self.client.post( + reverse("account_password"), + post_data + ) + # history count should be zero + self.assertEquals(self.user.password_history.count(), 0) diff --git a/account/views.py b/account/views.py index 9d3e7022..e1faab21 100644 --- a/account/views.py +++ b/account/views.py @@ -22,7 +22,7 @@ from account.hooks import hookset from account.mixins import LoginRequiredMixin from account.models import SignupCode, EmailAddress, EmailConfirmation, Account, AccountDeletion, PasswordHistory -from account.utils import check_password_expired, default_redirect, get_form_data +from account.utils import default_redirect, get_form_data class PasswordMixin(object): @@ -344,11 +344,6 @@ class LoginView(FormView): def get(self, *args, **kwargs): if self.request.user.is_authenticated(): - - # Check for password expiration, redirect if needed. - if check_password_expired(self.request.user): - return redirect("account_password") - return redirect(self.get_success_url()) return super(LoginView, self).get(*args, **kwargs) diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 1ed54927..00000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. _changelog: - -CHANGELOG -========= - -2.0 ---- - -* add password expiration -* add Django v1.10 to test compatibility matrix -* remove Python 3.2 from text compatibility matrix - -1.0 ---- - -* initial release -* if migrating from Pinax; see :ref:`migration` diff --git a/docs/index.rst b/docs/index.rst index 1cf5cf2f..0a97be34 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,5 @@ Contents settings templates signals - changelog migration faq diff --git a/docs/usage.rst b/docs/usage.rst index 1e52d17d..9786a165 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -260,10 +260,17 @@ Enabling password expiration ============================ Password expiration is disabled by default. In order to enable password expiration -you must add two entries to your settings file:: +you must add entries to your settings file:: - PASSWORD_EXPIRY = 60*60*24*5 # seconds until pw expires, this example shows five days - PASSWORD_USE_HISTORY = True + ACCOUNT_PASSWORD_EXPIRY = 60*60*24*5 # seconds until pw expires, this example shows five days + ACCOUNT_PASSWORD_USE_HISTORY = True + +and include `ExpiredPasswordMiddleware` with your middleware settings:: + + MIDDLEWARE_CLASSES = { + ... + "account.middleware.ExpiredPasswordMiddleware", + } PASSWORD_EXPIRY indicates the duration a password will stay valid. After that period the password must be reset in order to log in. If PASSWORD_EXPIRY is zero (0) @@ -276,3 +283,6 @@ If PASSWORD_USE_HISTORY is True, a password history entry is created each time the user changes their password. This entry links the user with their most recent (encrypted) password and a timestamp. Unless deleted manually, PasswordHistory items are saved forever, allowing password history checking for new passwords. + +For an authenticated user, `ExpiredPasswordMiddleware` prevents retrieving or posting +to any page (except the password change page!) when the user password is expired. From d20a52304a09db454cfb144a48eaaf4eb19c4914 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 12:36:55 -0600 Subject: [PATCH 029/239] Fix documentation formatting --- docs/usage.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 9786a165..06577521 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -272,17 +272,17 @@ and include `ExpiredPasswordMiddleware` with your middleware settings:: "account.middleware.ExpiredPasswordMiddleware", } -PASSWORD_EXPIRY indicates the duration a password will stay valid. After that period -the password must be reset in order to log in. If PASSWORD_EXPIRY is zero (0) +``ACCOUNT_PASSWORD_EXPIRY`` indicates the duration a password will stay valid. After that period +the password must be reset in order to log in. If ``ACCOUNT_PASSWORD_EXPIRY`` is zero (0) then passwords never expire. -If PASSWORD_USE_HISTORY is False, no history will be generated and password +If ``ACCOUNT_PASSWORD_USE_HISTORY`` is False, no history will be generated and password expiration WILL NOT be checked. -If PASSWORD_USE_HISTORY is True, a password history entry is created each time +If ``ACCOUNT_PASSWORD_USE_HISTORY`` is True, a password history entry is created each time the user changes their password. This entry links the user with their most recent (encrypted) password and a timestamp. Unless deleted manually, PasswordHistory items are saved forever, allowing password history checking for new passwords. -For an authenticated user, `ExpiredPasswordMiddleware` prevents retrieving or posting +For an authenticated user, ``ExpiredPasswordMiddleware`` prevents retrieving or posting to any page (except the password change page!) when the user password is expired. From c2b3316cf360d5cdf2b5e21a08dc5733c796f77d Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 12:39:31 -0600 Subject: [PATCH 030/239] Update installation docs for pw expiration --- docs/installation.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index a34c9206..44d09223 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -45,6 +45,15 @@ Add ``account.middleware.LocaleMiddleware`` and ... ] +Optionally include ``account.middleware.ExpiredPasswordMiddleware`` in +``MIDDLEWARE_CLASSES`` if you need password expiration support:: + + MIDDLEWARE_CLASSES = [ + ... + "account.middleware.ExpiredPasswordMiddleware", + ... + ] + Once everything is in place make sure you run ``migrate`` to modify the database with the ``account`` app models. From ac43310330c2fa2d8a734a1b5705151e88ac42e0 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 12:40:50 -0600 Subject: [PATCH 031/239] Improve verbiage --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 06577521..34005e63 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -273,7 +273,7 @@ and include `ExpiredPasswordMiddleware` with your middleware settings:: } ``ACCOUNT_PASSWORD_EXPIRY`` indicates the duration a password will stay valid. After that period -the password must be reset in order to log in. If ``ACCOUNT_PASSWORD_EXPIRY`` is zero (0) +the password must be reset in order to view any page. If ``ACCOUNT_PASSWORD_EXPIRY`` is zero (0) then passwords never expire. If ``ACCOUNT_PASSWORD_USE_HISTORY`` is False, no history will be generated and password From 527b9cc5c74d9cc979e2fc7655be25c5f5df48f4 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 12:43:51 -0600 Subject: [PATCH 032/239] Improve comments --- account/tests/test_password.py | 6 +++--- account/utils.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 42b19f03..52a53d77 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -81,7 +81,7 @@ def test_get_not_expired(self): """ self.client.login(username=self.username, password=self.password) - # get account settings page + # get account settings page (could be any application page) response = self.client.get(reverse("account_settings")) self.assertEquals(response.status_code, 200) @@ -96,7 +96,7 @@ def test_get_expired(self): self.client.login(username=self.username, password=self.password) - # get account settings page + # get account settings page (could be any application page) response = self.client.get(reverse("account_settings")) self.assertRedirects(response, reverse("account_password")) @@ -158,7 +158,7 @@ def test_get_no_history(self): with override_settings( ACCOUNT_PASSWORD_USE_HISTORY=True ): - # get account settings page + # get account settings page (could be any application page) response = self.client.get(reverse("account_settings")) self.assertEquals(response.status_code, 200) diff --git a/account/utils.py b/account/utils.py index 783f6bcb..cb084d5a 100644 --- a/account/utils.py +++ b/account/utils.py @@ -113,7 +113,8 @@ def get_form_data(form, field_name, default=None): def check_password_expired(user): """ - Return True if password is expired, False otherwise. + Return True if password is expired and system is using + password expiration, False otherwise. """ if not settings.ACCOUNT_PASSWORD_USE_HISTORY: return False From f69f7982e2d0e9c598c07b6eb0432f60a6dd3fcc Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 13:56:12 -0600 Subject: [PATCH 033/239] Add messages and signals Add "Password is expired" message. Add `password_expired` signal. Update signals documentation. --- account/middleware.py | 7 +++++++ account/signals.py | 1 + docs/signals.rst | 7 +++++++ 3 files changed, 15 insertions(+) diff --git a/account/middleware.py b/account/middleware.py index 5118af63..ea89737f 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals +from django.contrib import messages from django.core.urlresolvers import resolve from django.shortcuts import redirect from django.utils import translation, timezone from django.utils.cache import patch_vary_headers +from django.utils.translation import ugettext_lazy as _ from account.conf import settings from account.models import Account @@ -64,6 +66,11 @@ def process_request(self, request): # All users must be allowed to access "change password" url. if url_name not in settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL: if check_password_expired(request.user): + messages.add_message( + request, + messages.WARNING, + _("Password is expired.") + ) return redirect( settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL ) diff --git a/account/signals.py b/account/signals.py index 307c8cea..8bc14a96 100644 --- a/account/signals.py +++ b/account/signals.py @@ -12,3 +12,4 @@ email_confirmed = django.dispatch.Signal(providing_args=["email_address"]) email_confirmation_sent = django.dispatch.Signal(providing_args=["confirmation"]) password_changed = django.dispatch.Signal(providing_args=["user"]) +password_expired = django.dispatch.Signal(providing_args=["user"]) diff --git a/docs/signals.rst b/docs/signals.rst index 57d1f7b0..9b7b8f09 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -66,4 +66,11 @@ password_changed ---------------- Triggered when a user changes his password. Providing argument ``user`` +(User instance). + + +password_expired +---------------- + +Triggered when a user password is expired. Providing argument ``user`` (User instance). \ No newline at end of file From 06c4309c5b96c2995f91d676f6bbcec98c0b4421 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 14:01:48 -0600 Subject: [PATCH 034/239] Send signal for password expired Improve verbose plural model name. --- account/middleware.py | 2 ++ account/models.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/account/middleware.py b/account/middleware.py index ea89737f..233cd384 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -7,6 +7,7 @@ from django.utils.cache import patch_vary_headers from django.utils.translation import ugettext_lazy as _ +from account import signals from account.conf import settings from account.models import Account from account.utils import check_password_expired @@ -66,6 +67,7 @@ def process_request(self, request): # All users must be allowed to access "change password" url. if url_name not in settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL: if check_password_expired(request.user): + signals.password_expired.send(sender=self, user=request.user) messages.add_message( request, messages.WARNING, diff --git a/account/models.py b/account/models.py index 345f0d47..9b18c92c 100644 --- a/account/models.py +++ b/account/models.py @@ -396,6 +396,10 @@ class PasswordHistory(models.Model): """ Contains single password history for user. """ + class Meta: + verbose_name = _("password history") + verbose_name_plural = _("password histories") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history") password = models.CharField(max_length=255) # encrypted password timestamp = models.DateTimeField(default=timezone.now) # password creation time From 2954278a1abc6cd37066cb6476c36c1b3489e56e Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 14:32:55 -0600 Subject: [PATCH 035/239] Improve admin display, expired pw message --- account/admin.py | 3 +++ account/middleware.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/account/admin.py b/account/admin.py index 3f01f5ce..48a0ce6a 100644 --- a/account/admin.py +++ b/account/admin.py @@ -44,6 +44,9 @@ class PasswordExpiryAdmin(admin.ModelAdmin): class PasswordHistoryAdmin(admin.ModelAdmin): raw_id_fields = ["user"] + list_display = ["user", "timestamp"] + list_filter = ["user"] + ordering = ["user__username", "-timestamp"] admin.site.register(Account, AccountAdmin) diff --git a/account/middleware.py b/account/middleware.py index 233cd384..7ab701eb 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -71,7 +71,7 @@ def process_request(self, request): messages.add_message( request, messages.WARNING, - _("Password is expired.") + _("Your password has expired. Please save a new password.") ) return redirect( settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL From 127613fe9f176047cdd9ebf88be8b6102d29e762 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 15:25:28 -0600 Subject: [PATCH 036/239] Add "?next=" when expired password detected If middleware detects expired password, add "?next=" to redirect URL with expected page. --- account/middleware.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index 7ab701eb..18fa5d0c 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.contrib import messages -from django.core.urlresolvers import resolve +from django.core.urlresolvers import resolve, reverse from django.shortcuts import redirect from django.utils import translation, timezone from django.utils.cache import patch_vary_headers @@ -65,7 +65,9 @@ def process_request(self, request): if request.user.is_authenticated() and not request.user.is_staff: url_name = resolve(request.path).url_name # All users must be allowed to access "change password" url. - if url_name not in settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL: + if url_name not in [ + settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, + ]: if check_password_expired(request.user): signals.password_expired.send(sender=self, user=request.user) messages.add_message( @@ -73,6 +75,6 @@ def process_request(self, request): messages.WARNING, _("Your password has expired. Please save a new password.") ) - return redirect( - settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL - ) + redirect_url = "{}?next={}".format(reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL), url_name) + + return redirect(redirect_url) From 3108ddb53b596b91b71260edeb0a008c46f17e9c Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 15:35:34 -0600 Subject: [PATCH 037/239] Update test to check "?next=" in URL --- account/tests/test_password.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 52a53d77..5aa7af43 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -97,8 +97,12 @@ def test_get_expired(self): self.client.login(username=self.username, password=self.password) # get account settings page (could be any application page) - response = self.client.get(reverse("account_settings")) - self.assertRedirects(response, reverse("account_password")) + url_name = "account_settings" + response = self.client.get(reverse(url_name)) + + # verify desired page is set as "?next=" in redirect URL + redirect_url = "{}?next={}".format(reverse("account_password"), url_name) + self.assertRedirects(response, redirect_url) def test_password_expiration_reset(self): """ From 40d419d7125ebef13ace9e208f3858c8e55ec75d Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 16:46:08 -0600 Subject: [PATCH 038/239] Add ACCOUNT_LOGOUT_URL Improve creation of redirect URL with "next" value. --- account/conf.py | 1 + account/middleware.py | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/account/conf.py b/account/conf.py index 352b1f60..df3f0155 100644 --- a/account/conf.py +++ b/account/conf.py @@ -32,6 +32,7 @@ class AccountAppConf(AppConf): OPEN_SIGNUP = True LOGIN_URL = "account_login" + LOGOUT_URL = "account_logout" SIGNUP_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/" diff --git a/account/middleware.py b/account/middleware.py index 18fa5d0c..79761cfb 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,8 +1,13 @@ from __future__ import unicode_literals +try: + from urllib.parse import urlparse, urlunparse +except ImportError: # python 2 + from urlparse import urlparse, urlunparse + from django.contrib import messages from django.core.urlresolvers import resolve, reverse -from django.shortcuts import redirect +from django.http import HttpResponseRedirect, QueryDict from django.utils import translation, timezone from django.utils.cache import patch_vary_headers from django.utils.translation import ugettext_lazy as _ @@ -63,11 +68,13 @@ class ExpiredPasswordMiddleware(object): def process_request(self, request): if request.user.is_authenticated() and not request.user.is_staff: - url_name = resolve(request.path).url_name - # All users must be allowed to access "change password" url. - if url_name not in [ - settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, - ]: + next_url = resolve(request.path).url_name + # Authenticated users must be allowed to access + # "change password" page and "log out" page. + # even if password is expired. + if next_url not in [settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, + settings.ACCOUNT_LOGOUT_URL, + ]: if check_password_expired(request.user): signals.password_expired.send(sender=self, user=request.user) messages.add_message( @@ -75,6 +82,12 @@ def process_request(self, request): messages.WARNING, _("Your password has expired. Please save a new password.") ) - redirect_url = "{}?next={}".format(reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL), url_name) + redirect_field_name = "next" # fragile! + + change_password_url = reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL) + url_bits = list(urlparse(change_password_url)) + querystring = QueryDict(url_bits[4], mutable=True) + querystring[redirect_field_name] = next_url + url_bits[4] = querystring.urlencode(safe="/") - return redirect(redirect_url) + return HttpResponseRedirect(urlunparse(url_bits)) From 83c9efca07be07aa6ca6a20931fa142c988d6c60 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 17:19:53 -0600 Subject: [PATCH 039/239] Add note about "staff" user skipping pw check --- docs/usage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index 34005e63..7ccd109c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -285,4 +285,5 @@ the user changes their password. This entry links the user with their most recen are saved forever, allowing password history checking for new passwords. For an authenticated user, ``ExpiredPasswordMiddleware`` prevents retrieving or posting -to any page (except the password change page!) when the user password is expired. +to any page except the password change page and log out page when the user password is expired. +However, if the user is "staff" (can access the Django admin site), the password check is skipped. From b557fd70193842a79dd37eb166fa1867e613c94e Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Tue, 13 Sep 2016 18:32:37 -0600 Subject: [PATCH 040/239] Update version: "2.0.0.dev2" --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 27916c74..6e9bc966 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0.dev1" +__version__ = "2.0.0.dev2" From c82b67ef5b398b94da263d94ff6d06542d465707 Mon Sep 17 00:00:00 2001 From: Jonathan Potter Date: Wed, 14 Sep 2016 18:45:25 -0400 Subject: [PATCH 041/239] Validate email address case insensitive uniqueness at the model level --- account/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/account/models.py b/account/models.py index 047609b7..db676426 100644 --- a/account/models.py +++ b/account/models.py @@ -8,6 +8,7 @@ except ImportError: # python 2 from urllib import urlencode +from django import forms from django.core.urlresolvers import reverse from django.db import models, transaction from django.db.models import Q @@ -301,6 +302,16 @@ def change(self, new_email, confirm=True): if confirm: self.send_confirmation() + def validate_unique(self, exclude=None): + super(EmailAddress, self).validate_unique(exclude=exclude) + + qs = EmailAddress.objects.filter(email__iexact=self.email) + + if qs.exists() and settings.ACCOUNT_EMAIL_UNIQUE: + raise forms.ValidationError({ + 'email': _("A user is registered with this email address."), + }) + @python_2_unicode_compatible class EmailConfirmation(models.Model): From 1f44952954b92b7f96f53556325445765e8e0981 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 15 Sep 2016 09:47:20 -0600 Subject: [PATCH 042/239] Add management command documentation --- docs/commands.rst | 37 +++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 38 insertions(+) create mode 100644 docs/commands.rst diff --git a/docs/commands.rst b/docs/commands.rst new file mode 100644 index 00000000..da4fdf40 --- /dev/null +++ b/docs/commands.rst @@ -0,0 +1,37 @@ +.. _commands: + +=================== +Management Commands +=================== + +user_password_history +--------------------- + +Creates an initial password history for all users who don't already +have a password history. + +Accepts two optional arguments:: + + -d --days - Sets the age of the current password. Default is 10 days. + -f --force - Sets a new password history for ALL users, regardless of prior history. + +user_password_expiry +-------------------- + +Creates a password expiry specific to one user. + +Password expiration checks use a global value (``ACCOUNT_PASSWORD_EXPIRY``) +for the expiration time period. This value can be superseded on a per-user basis +by creating a user password expiry. + +Requires one argument:: + + - username of the user who needs specific password expiry. + +Accepts one optional argument:: + + -e --expire - Sets the number of seconds for password expiration. + Default is the current global ACCOUNT_PASSWORD_EXPIRY value. + +After creation, you can modify the user's password expiration from the Django +admin, look for "account.PasswordExpiry" and find the desired user. diff --git a/docs/index.rst b/docs/index.rst index 0a97be34..27219b30 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,5 +21,6 @@ Contents settings templates signals + commands migration faq From 55ae16056bbc4b2dc7ad10f3381097fc0fbd3a81 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 15 Sep 2016 10:07:44 -0600 Subject: [PATCH 043/239] Fix docs --- docs/commands.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/commands.rst b/docs/commands.rst index da4fdf40..f0ab80fe 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -26,12 +26,12 @@ by creating a user password expiry. Requires one argument:: - - username of the user who needs specific password expiry. + [] - username(s) of the user(s) who needs specific password expiry. Accepts one optional argument:: -e --expire - Sets the number of seconds for password expiration. Default is the current global ACCOUNT_PASSWORD_EXPIRY value. -After creation, you can modify the user's password expiration from the Django -admin, look for "account.PasswordExpiry" and find the desired user. +After creation, you can modify user password expiration from the Django +admin. for "account.PasswordExpiry" and find the desired user. From 1e0d672dea62146837e3e2e5b76dda30d718fbbf Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 15 Sep 2016 10:12:18 -0600 Subject: [PATCH 044/239] Improve directions --- docs/commands.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.rst b/docs/commands.rst index f0ab80fe..7004401e 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -34,4 +34,4 @@ Accepts one optional argument:: Default is the current global ACCOUNT_PASSWORD_EXPIRY value. After creation, you can modify user password expiration from the Django -admin. for "account.PasswordExpiry" and find the desired user. +admin. Find the desired user at ``/admin/account/passwordexpiry/`` and change the ``expiry`` value. From d16099bb24e728665b637b9e94b8f117880e6423 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 15 Sep 2016 10:31:09 -0600 Subject: [PATCH 045/239] Improve commands doc --- docs/commands.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/commands.rst b/docs/commands.rst index 7004401e..1e4288c4 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -12,7 +12,7 @@ have a password history. Accepts two optional arguments:: - -d --days - Sets the age of the current password. Default is 10 days. + -d --days - Sets the age of the current password. Default is 10 days. -f --force - Sets a new password history for ALL users, regardless of prior history. user_password_expiry @@ -30,8 +30,8 @@ Requires one argument:: Accepts one optional argument:: - -e --expire - Sets the number of seconds for password expiration. - Default is the current global ACCOUNT_PASSWORD_EXPIRY value. + -e --expire - Sets the number of seconds for password expiration. + Default is the current global ACCOUNT_PASSWORD_EXPIRY value. After creation, you can modify user password expiration from the Django admin. Find the desired user at ``/admin/account/passwordexpiry/`` and change the ``expiry`` value. From 32b7623237c22cf51556cbcb1d0837018ee5ce03 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Mon, 19 Sep 2016 10:21:48 -0600 Subject: [PATCH 046/239] Use django.contrib.auth.REDIRECT_FIELD_NAME Allow customization, but use REDIRECT_FIELD_NAME as default. --- account/decorators.py | 3 ++- account/middleware.py | 3 ++- account/mixins.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index fd349a8f..46ddcdd7 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -2,12 +2,13 @@ import functools +from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.decorators import available_attrs from account.utils import handle_redirect_to_login -def login_required(func=None, redirect_field_name="next", login_url=None): +def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): """ Decorator for views that checks that the user is logged in, redirecting to the log in page if necessary. diff --git a/account/middleware.py b/account/middleware.py index 79761cfb..b073645b 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -6,6 +6,7 @@ from urlparse import urlparse, urlunparse from django.contrib import messages +from django.contrib.auth import REDIRECT_FIELD_NAME from django.core.urlresolvers import resolve, reverse from django.http import HttpResponseRedirect, QueryDict from django.utils import translation, timezone @@ -82,7 +83,7 @@ def process_request(self, request): messages.WARNING, _("Your password has expired. Please save a new password.") ) - redirect_field_name = "next" # fragile! + redirect_field_name = REDIRECT_FIELD_NAME change_password_url = reverse(settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL) url_bits = list(urlparse(change_password_url)) diff --git a/account/mixins.py b/account/mixins.py index 6444e185..240926dc 100644 --- a/account/mixins.py +++ b/account/mixins.py @@ -1,12 +1,14 @@ from __future__ import unicode_literals +from django.contrib.auth import REDIRECT_FIELD_NAME + from account.conf import settings from account.utils import handle_redirect_to_login class LoginRequiredMixin(object): - redirect_field_name = "next" + redirect_field_name = REDIRECT_FIELD_NAME login_url = None def dispatch(self, request, *args, **kwargs): From 2f5114dcea6fb96cb057f87dcb8784568a3309e7 Mon Sep 17 00:00:00 2001 From: lampslave Date: Tue, 20 Sep 2016 04:12:11 +0300 Subject: [PATCH 047/239] fix language codes --- account/languages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/account/languages.py b/account/languages.py index 1a280aa6..cc9a5cce 100644 --- a/account/languages.py +++ b/account/languages.py @@ -27,7 +27,7 @@ ("de", "Deutsch"), ("el", "Ελληνικά"), ("en", "English"), - ("en-a", "Australian English"), + ("en-au", "Australian English"), ("en-gb", "British English"), ("eo", "Esperanto"), ("es", "español"), @@ -36,7 +36,7 @@ ("es-ni", "español de Nicaragua"), ("es-ve", "español de Venezuela"), ("et", "eesti"), - ("e", "Basque"), + ("eu", "Basque"), ("fa", "فارسی"), ("fi", "suomi"), ("fr", "français"), @@ -46,7 +46,7 @@ ("he", "עברית"), ("hi", "Hindi"), ("hr", "Hrvatski"), - ("h", "Magyar"), + ("hu", "Magyar"), ("ia", "Interlingua"), ("id", "Bahasa Indonesia"), ("io", "ido"), @@ -76,7 +76,7 @@ ("pt", "Português"), ("pt-br", "Português Brasileiro"), ("ro", "Română"), - ("r", "Русский"), + ("ru", "Русский"), ("sk", "slovenský"), ("sl", "Slovenščina"), ("sq", "shqip"), From 3af890ab35b392cd75117fd85f5a5be3ff8dd1d9 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 23 Sep 2016 11:59:11 -0600 Subject: [PATCH 048/239] Added Django 1.10 to Travis testing --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1741e05f..8d4420af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: env: - DJANGO=1.8 - DJANGO=1.9 + - DJANGO=1.10 - DJANGO=master matrix: exclude: From 586399effdcdf7d8678743ff4f38829f6cdbe2de Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 23 Sep 2016 12:03:06 -0600 Subject: [PATCH 049/239] Exclude Python 3.3 on Django 1.10 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8d4420af..6b4a8e2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ matrix: exclude: - python: "3.3" env: DJANGO=1.9 + - python: "3.3" + env: DJANGO=1.10 - python: "3.3" env: DJANGO=master install: From 17e90c77ae4b13a5345fe18b91354b021a88da8c Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:00:32 -0600 Subject: [PATCH 050/239] Cleaned up 2.0.0 changelog entries --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4170750..b075b0bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,14 @@ version with these. Your code will need to be updated to continue working. ## 2.0.0 - * BI: account deletion callbacks moved to hooksets + * BI: moved account deletion callbacks to hooksets * BI: dropped Django 1.7 support + * BI: dropped Python 3.2 support * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value - * BI: remove Python 3.2 from test compatibility matrix - * add Django v1.10 to test compatibility matrix + * added Django v1.10 to test compatibility matrix * added Turkish translations * fixed migration with language codes to dynamically set - * add password expiration + * added password expiration ## 1.3.0 From 1392a0a30a5c0df26aa0ac740e8814de50d54cf1 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:01:07 -0600 Subject: [PATCH 051/239] Explicitly state 1.10 support --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b075b0bf..97e4f460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ version with these. Your code will need to be updated to continue working. * BI: dropped Django 1.7 support * BI: dropped Python 3.2 support * BI: removed deprecated `ACCOUNT_USE_AUTH_AUTHENTICATE` setting with behavior matching its `True` value - * added Django v1.10 to test compatibility matrix + * added Django 1.10 support * added Turkish translations * fixed migration with language codes to dynamically set * added password expiration From b98cbc652e01f7bf441d0e66f2e158809e6893c3 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:04:39 -0600 Subject: [PATCH 052/239] Added Django 1.10 middleware support --- account/compat.py | 16 ++++++++++++++++ account/middleware.py | 7 ++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 account/compat.py diff --git a/account/compat.py b/account/compat.py new file mode 100644 index 00000000..9e17dacf --- /dev/null +++ b/account/compat.py @@ -0,0 +1,16 @@ + +class MiddlewareMixin(object): + + def __init__(self, get_response=None): + self.get_response = get_response + super(MiddlewareMixin, self).__init__() + + def __call__(self, request): + response = None + if hasattr(self, "process_request"): + response = self.process_request(request) + if not response: + response = self.get_response(request) + if hasattr(self, "process_response"): + response = self.process_response(request, response) + return response diff --git a/account/middleware.py b/account/middleware.py index b073645b..3556c4c0 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -14,12 +14,13 @@ from django.utils.translation import ugettext_lazy as _ from account import signals +from account.compat import MiddlewareMixin from account.conf import settings from account.models import Account from account.utils import check_password_expired -class LocaleMiddleware(object): +class LocaleMiddleware(MiddlewareMixin): """ This is a very simple middleware that parses a request and decides what translation object to install in the current @@ -48,7 +49,7 @@ def process_response(self, request, response): return response -class TimezoneMiddleware(object): +class TimezoneMiddleware(MiddlewareMixin): """ This middleware sets the timezone used to display dates in templates to the user's timezone. @@ -65,7 +66,7 @@ def process_request(self, request): timezone.activate(tz) -class ExpiredPasswordMiddleware(object): +class ExpiredPasswordMiddleware(MiddlewareMixin): def process_request(self, request): if request.user.is_authenticated() and not request.user.is_staff: From 7012eb051e797ff4112f5aa5694bff34a3ca2fce Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:13:11 -0600 Subject: [PATCH 053/239] Revert "Added Django 1.10 middleware support" This reverts commit b98cbc652e01f7bf441d0e66f2e158809e6893c3. --- account/compat.py | 16 ---------------- account/middleware.py | 7 +++---- 2 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 account/compat.py diff --git a/account/compat.py b/account/compat.py deleted file mode 100644 index 9e17dacf..00000000 --- a/account/compat.py +++ /dev/null @@ -1,16 +0,0 @@ - -class MiddlewareMixin(object): - - def __init__(self, get_response=None): - self.get_response = get_response - super(MiddlewareMixin, self).__init__() - - def __call__(self, request): - response = None - if hasattr(self, "process_request"): - response = self.process_request(request) - if not response: - response = self.get_response(request) - if hasattr(self, "process_response"): - response = self.process_response(request, response) - return response diff --git a/account/middleware.py b/account/middleware.py index 3556c4c0..b073645b 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -14,13 +14,12 @@ from django.utils.translation import ugettext_lazy as _ from account import signals -from account.compat import MiddlewareMixin from account.conf import settings from account.models import Account from account.utils import check_password_expired -class LocaleMiddleware(MiddlewareMixin): +class LocaleMiddleware(object): """ This is a very simple middleware that parses a request and decides what translation object to install in the current @@ -49,7 +48,7 @@ def process_response(self, request, response): return response -class TimezoneMiddleware(MiddlewareMixin): +class TimezoneMiddleware(object): """ This middleware sets the timezone used to display dates in templates to the user's timezone. @@ -66,7 +65,7 @@ def process_request(self, request): timezone.activate(tz) -class ExpiredPasswordMiddleware(MiddlewareMixin): +class ExpiredPasswordMiddleware(object): def process_request(self, request): if request.user.is_authenticated() and not request.user.is_staff: From 74434fc5453c19efa165edc078c0a25410fd4999 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:13:32 -0600 Subject: [PATCH 054/239] Added Django 1.10 middleware support This commit is from previous work. --- account/middleware.py | 14 +++++++++++--- account/tests/test_password.py | 22 ++++++++++++++++------ runtests.py | 13 +++++++------ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index b073645b..e54decd8 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -5,6 +5,8 @@ except ImportError: # python 2 from urlparse import urlparse, urlunparse +import django + from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.core.urlresolvers import resolve, reverse @@ -19,7 +21,13 @@ from account.utils import check_password_expired -class LocaleMiddleware(object): +if django.VERSION >= (1, 10): + from django.utils.deprecation import MiddlewareMixin as BaseMiddleware +else: + BaseMiddleware = object + + +class LocaleMiddleware(BaseMiddleware): """ This is a very simple middleware that parses a request and decides what translation object to install in the current @@ -48,7 +56,7 @@ def process_response(self, request, response): return response -class TimezoneMiddleware(object): +class TimezoneMiddleware(BaseMiddleware): """ This middleware sets the timezone used to display dates in templates to the user's timezone. @@ -65,7 +73,7 @@ def process_request(self, request): timezone.activate(tz) -class ExpiredPasswordMiddleware(object): +class ExpiredPasswordMiddleware(BaseMiddleware): def process_request(self, request): if request.user.is_authenticated() and not request.user.is_staff: diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 5aa7af43..e05cce3b 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -1,6 +1,8 @@ import datetime import pytz +import django + from django.contrib.auth.hashers import ( check_password, make_password, @@ -20,13 +22,21 @@ from ..utils import check_password_expired +def middleware_kwarg(value): + if django.VERSION >= (1, 10): + kwarg = "MIDDLEWARE" + else: + kwarg = "MIDDLEWARE_CLASSES" + return {kwarg: value} + + @override_settings( ACCOUNT_PASSWORD_USE_HISTORY=True ) @modify_settings( - MIDDLEWARE_CLASSES={ - 'append': 'account.middleware.ExpiredPasswordMiddleware' - } + **middleware_kwarg({ + "append": "account.middleware.ExpiredPasswordMiddleware" + }) ) class PasswordExpirationTestCase(TestCase): @@ -133,9 +143,9 @@ def test_password_expiration_reset(self): @modify_settings( - MIDDLEWARE_CLASSES={ - 'append': 'account.middleware.ExpiredPasswordMiddleware' - } + **middleware_kwarg({ + "append": "account.middleware.ExpiredPasswordMiddleware" + }) ) class ExistingUserNoHistoryTestCase(TestCase): """ diff --git a/runtests.py b/runtests.py index 103f3e24..471171eb 100644 --- a/runtests.py +++ b/runtests.py @@ -19,12 +19,6 @@ "account", "account.tests", ], - MIDDLEWARE_CLASSES=[ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - ], DATABASES={ "default": { "ENGINE": "django.db.backends.sqlite3", @@ -58,6 +52,13 @@ ] ) +DEFAULT_SETTINGS["MIDDLEWARE" if django.VERSION >= (1, 10) else "MIDDLEWARE_CLASSES"] = [ + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", +] + def runtests(*test_args): if not settings.configured: From c58e26fdef800c95449267013ea07d44ea366a17 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:23:14 -0600 Subject: [PATCH 055/239] Updated MANIFEST.in --- MANIFEST.in | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index f1b8dd38..96465f3a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,11 @@ +include .coveragerc +include CHANGELOG.md +include CONTRIBUTING.md +include LICENSE +include tox.ini include README.rst include runtests.py +recursive-include account *.html +recursive-include account *.txt recursive-include account/locale * recursive-include docs Makefile conf.py *.rst From d1c1fa6f6a84debb4a48802ae6f692b82d4eb3ac Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:27:49 -0600 Subject: [PATCH 056/239] Bumped to 2.0.0 --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 6e9bc966..8c0d5d5b 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0.dev2" +__version__ = "2.0.0" From 591ecdfa344405aaa961d65298d37881a6f93c92 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:48:57 -0600 Subject: [PATCH 057/239] Added password whitespace stripping by default Fixes #222. This behavior makes the Django 1.9 default behavior on all supported versions of Django. --- account/conf.py | 1 + account/forms.py | 29 +++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/account/conf.py b/account/conf.py index df3f0155..c05380cc 100644 --- a/account/conf.py +++ b/account/conf.py @@ -40,6 +40,7 @@ class AccountAppConf(AppConf): PASSWORD_RESET_REDIRECT_URL = "account_login" PASSWORD_EXPIRY = 0 PASSWORD_USE_HISTORY = False + PASSWORD_STRIP = True REMEMBER_ME_EXPIRY = 60 * 60 * 24 * 365 * 10 USER_DISPLAY = lambda user: user.username # flake8: noqa CREATE_ON_SAVE = True diff --git a/account/forms.py b/account/forms.py index d807e247..4aa0cb92 100644 --- a/account/forms.py +++ b/account/forms.py @@ -8,6 +8,7 @@ OrderedDict = None from django import forms +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.contrib import auth @@ -22,6 +23,22 @@ alnum_re = re.compile(r"^\w+$") +class PasswordField(forms.CharField): + + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", forms.PasswordInput(render_value=False)) + self.strip = kwargs.pop("strip", True) + super(PasswordField, self).__init__(*args, **kwargs) + + def to_python(self, value): + if value in self.empty_values: + return "" + value = force_text(value) + if self.strip: + value = value.strip() + return value + + class SignupForm(forms.Form): username = forms.CharField( @@ -30,13 +47,13 @@ class SignupForm(forms.Form): widget=forms.TextInput(), required=True ) - password = forms.CharField( + password = PasswordField( label=_("Password"), - widget=forms.PasswordInput(render_value=False) + strip=settings.ACCOUNT_PASSWORD_STRIP, ) - password_confirm = forms.CharField( + password_confirm = PasswordField( label=_("Password (again)"), - widget=forms.PasswordInput(render_value=False) + strip=settings.ACCOUNT_PASSWORD_STRIP, ) email = forms.EmailField( label=_("Email"), @@ -76,9 +93,9 @@ def clean(self): class LoginForm(forms.Form): - password = forms.CharField( + password = PasswordField( label=_("Password"), - widget=forms.PasswordInput(render_value=False) + strip=settings.ACCOUNT_PASSWORD_STRIP, ) remember = forms.BooleanField( label=_("Remember Me"), From 7f38b4960f55176c583712b521c556347b46c68c Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 10:51:44 -0600 Subject: [PATCH 058/239] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e4f460..f5c29b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ version with these. Your code will need to be updated to continue working. * added Turkish translations * fixed migration with language codes to dynamically set * added password expiration + * added password stripping by default ## 1.3.0 From c6f6b5adcc09cf06ff81f110b17839f11c52b189 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 11:23:33 -0600 Subject: [PATCH 059/239] Added ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN feature Fixes #130. --- CHANGELOG.md | 1 + account/conf.py | 1 + account/views.py | 11 ++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c29b1e..9a82c1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ version with these. Your code will need to be updated to continue working. * fixed migration with language codes to dynamically set * added password expiration * added password stripping by default + * added `ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN` feature (default is `False`) ## 1.3.0 diff --git a/account/conf.py b/account/conf.py index c05380cc..4e620af2 100644 --- a/account/conf.py +++ b/account/conf.py @@ -48,6 +48,7 @@ class AccountAppConf(AppConf): EMAIL_CONFIRMATION_REQUIRED = False EMAIL_CONFIRMATION_EMAIL = True EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 + EMAIL_CONFIRMATION_AUTO_LOGIN = False EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "account_login" EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None EMAIL_CONFIRMATION_URL = "account_confirm_email" diff --git a/account/views.py b/account/views.py index e1faab21..e1513599 100644 --- a/account/views.py +++ b/account/views.py @@ -458,6 +458,10 @@ def post(self, *args, **kwargs): self.object = confirmation = self.get_object() confirmation.confirm() self.after_confirmation(confirmation) + if settings.ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN: + self.user = self.login_user(confirmation.email_address.user) + else: + self.user = self.request.user redirect_url = self.get_redirect_url() if not redirect_url: ctx = self.get_context_data() @@ -491,7 +495,7 @@ def get_context_data(self, **kwargs): return ctx def get_redirect_url(self): - if self.request.user.is_authenticated(): + if self.user.is_authenticated(): if not settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL: return settings.ACCOUNT_LOGIN_REDIRECT_URL return settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL @@ -503,6 +507,11 @@ def after_confirmation(self, confirmation): user.is_active = True user.save() + def login_user(self, user): + user.backend = "django.contrib.auth.backends.ModelBackend" + auth.login(self.request, user) + return user + class ChangePasswordView(PasswordMixin, FormView): From 676f428546e918189995cbaf0711b459c298fb66 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Tue, 18 Oct 2016 11:34:46 -0600 Subject: [PATCH 060/239] Added test for auto login after email confirmation --- account/tests/test_views.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/account/tests/test_views.py b/account/tests/test_views.py index cddab22f..9444ea38 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -264,6 +264,20 @@ def test_post_not_required_redirect_override(self): fetch_redirect_response=False ) + @override_settings( + ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=True, + ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN=True, + ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL="/somewhere/", + ) + def test_post_auto_login(self): + email_confirmation = self.signup() + response = self.client.post(reverse("account_confirm_email", kwargs={"key": email_confirmation.key}), {}) + self.assertRedirects( + response, + settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL, + fetch_redirect_response=False + ) + class ChangePasswordViewTestCase(TestCase): From 67e76cc83f117ff20f260d25326775bcd58afc92 Mon Sep 17 00:00:00 2001 From: Jonathan Potter Date: Mon, 31 Oct 2016 14:16:52 -0400 Subject: [PATCH 061/239] Add test case for email unique validate --- account/tests/test_email_address.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 account/tests/test_email_address.py diff --git a/account/tests/test_email_address.py b/account/tests/test_email_address.py new file mode 100644 index 00000000..673e33a7 --- /dev/null +++ b/account/tests/test_email_address.py @@ -0,0 +1,25 @@ +from account.models import EmailAddress +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.forms import ValidationError +from django.test import TestCase, override_settings + + +@override_settings(ACCOUNT_EMAIL_UNIQUE=True) +class UniqueEmailAddressTestCase(TestCase): + def test_unique_email(self): + user = User.objects.create_user("user1", email="user1@example.com", password="password") + + email_1 = EmailAddress(user=user, email="user2@example.com") + email_1.full_clean() + email_1.save() + + validation_error = False + try: + email_2 = EmailAddress(user=user, email="USER2@example.com") + email_2.full_clean() + email_2.save() + except ValidationError: + validation_error = True + + self.assertTrue(validation_error) From 028b876da1d302e778b8e32a0af568b02de6057e Mon Sep 17 00:00:00 2001 From: Frank Lanitz Date: Fri, 4 Nov 2016 16:08:58 +0100 Subject: [PATCH 062/239] Correct link to source for available languages This change only updates a link inside documentation point to list of available languages. --- docs/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.rst b/docs/settings.rst index 6cb76db3..01a3a99b 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -147,4 +147,4 @@ Default: ``list(zip(pytz.all_timezones, pytz.all_timezones))`` ``ACCOUNT_LANGUAGES`` ===================== -See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/language_list.py +See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/languages.py From 87d2647415c021759ee337d4abaa9b4c3a260350 Mon Sep 17 00:00:00 2001 From: Seyi Ogunyemi Date: Fri, 25 Nov 2016 15:12:38 +0000 Subject: [PATCH 063/239] Avoid a KeyError. Resolved a KeyError issue by making the change above. It might be worthwhile to mention something related to this in the documentation. Appears to be also related to #227. Reference: https://github.com/pinax/django-user-accounts/blob/master/account/views.py#L290 --- docs/usage.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/usage.rst b/docs/usage.rst index 7ccd109c..b746e3b4 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -174,6 +174,7 @@ If you want to get rid of username you'll need to do some extra work: class SignupView(account.views.SignupView): form_class = myproject.forms.SignupForm + identifier_field = 'email' def generate_username(self, form): # do something to generate a unique username (required by the From 5456a74534f4b5755bc0af6ad621624c72fe69cd Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 3 Dec 2016 21:42:54 -0600 Subject: [PATCH 064/239] some translations --- account/locale/es/LC_MESSAGES/django.po | 165 ++++++++++++++++++------ 1 file changed, 122 insertions(+), 43 deletions(-) diff --git a/account/locale/es/LC_MESSAGES/django.po b/account/locale/es/LC_MESSAGES/django.po index dea3eaa2..74e29a19 100644 --- a/account/locale/es/LC_MESSAGES/django.po +++ b/account/locale/es/LC_MESSAGES/django.po @@ -1,7 +1,7 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # Erik Rivera , 2012 # Martin Gaitan , 2014 @@ -10,146 +10,225 @@ msgid "" msgstr "" "Project-Id-Version: django-user-accounts\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-07-30 15:12-0600\n" +"POT-Creation-Date: 2016-12-03 21:40-0600\n" "PO-Revision-Date: 2014-07-31 20:44+0000\n" "Last-Translator: Brian Rosner \n" -"Language-Team: Spanish (http://www.transifex.com/projects/p/django-user-accounts/language/es/)\n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/django-user-" +"accounts/language/es/)\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: forms.py:27 forms.py:107 +#: forms.py:45 forms.py:125 msgid "Username" msgstr "Nombre de usuario" -#: forms.py:33 forms.py:79 +#: forms.py:51 forms.py:97 msgid "Password" msgstr "Contraseña" -#: forms.py:37 +#: forms.py:55 msgid "Password (again)" msgstr "Contraseña (repetir)" -#: forms.py:41 forms.py:122 forms.py:168 forms.py:197 +#: forms.py:59 forms.py:140 forms.py:186 forms.py:215 msgid "Email" msgstr "Correo electrónico" -#: forms.py:52 +#: forms.py:70 msgid "Usernames can only contain letters, numbers and underscores." -msgstr "Los nombres de usuario solo pueden contener letras, números y subguiones" +msgstr "" +"Los nombres de usuario solo pueden contener letras, números y subguiones" -#: forms.py:60 +#: forms.py:78 msgid "This username is already taken. Please choose another." msgstr "Este nombre de usuario ya está en uso. Por favor elija otro." -#: forms.py:67 forms.py:217 +#: forms.py:85 forms.py:235 msgid "A user is registered with this email address." msgstr "Un usuario se ha registrado con esta dirección de correo electrónico." -#: forms.py:72 forms.py:162 forms.py:191 +#: forms.py:90 forms.py:180 forms.py:209 msgid "You must type the same password each time." msgstr "Debe escribir la misma contraseña cada vez." -#: forms.py:83 +#: forms.py:101 msgid "Remember Me" msgstr "Recordarme" -#: forms.py:96 +#: forms.py:114 msgid "This account is inactive." msgstr "Esta cuenta está inactiva." -#: forms.py:108 +#: forms.py:126 msgid "The username and/or password you specified are not correct." -msgstr "El nombre de usuario y/o la contraseña que ha especificado no son correctas." +msgstr "" +"El nombre de usuario y/o la contraseña que ha especificado no son correctas." -#: forms.py:123 +#: forms.py:141 msgid "The email address and/or password you specified are not correct." -msgstr "La dirección de correo electrónico y/o la contraseña que ha especificado no son correctas." +msgstr "" +"La dirección de correo electrónico y/o la contraseña que ha especificado no " +"son correctas." -#: forms.py:138 +#: forms.py:156 msgid "Current Password" msgstr "Contraseña actual" -#: forms.py:142 forms.py:180 +#: forms.py:160 forms.py:198 msgid "New Password" msgstr "Contraseña nueva" -#: forms.py:146 forms.py:184 +#: forms.py:164 forms.py:202 msgid "New Password (again)" msgstr "Contraseña nueva (repetir)" -#: forms.py:156 +#: forms.py:174 msgid "Please type your current password." msgstr "Por favor escriba su contraseña actual." -#: forms.py:173 +#: forms.py:191 msgid "Email address can not be found." msgstr "El email no pudo ser encontrado." -#: forms.py:199 +#: forms.py:217 msgid "Timezone" msgstr "Zona horaria" -#: forms.py:205 +#: forms.py:223 msgid "Language" msgstr "Idioma" -#: models.py:34 +#: middleware.py:92 +msgid "Your password has expired. Please save a new password." +msgstr "Tu contraseña expiró. Guarda tu Contraseña." + +#: models.py:36 models.py:412 msgid "user" msgstr "usuario" -#: models.py:35 +#: models.py:37 msgid "timezone" msgstr "zona horaria" -#: models.py:37 +#: models.py:39 msgid "language" msgstr "idioma" -#: models.py:250 +#: models.py:140 +msgid "code" +msgstr "código" + +#: models.py:141 +msgid "max uses" +msgstr "usos maximos" + +#: models.py:142 +msgid "expiry" +msgstr "expiró" + +#: models.py:145 +msgid "notes" +msgstr "notas" + +#: models.py:146 +msgid "sent" +msgstr "enviado" + +#: models.py:147 +msgid "created" +msgstr "creado" + +#: models.py:148 +msgid "use count" +msgstr "usos" + +#: models.py:151 +msgid "signup code" +msgstr "código " + +#: models.py:152 +msgid "signup codes" +msgstr "códigos de registro" + +#: models.py:259 +msgid "verified" +msgstr "verificado" + +#: models.py:260 +msgid "primary" +msgstr "primario" + +#: models.py:265 msgid "email address" msgstr "correo electrónico" -#: models.py:251 +#: models.py:266 msgid "email addresses" msgstr "correos electrónicos" -#: models.py:300 +#: models.py:316 msgid "email confirmation" msgstr "confirmación de correo electrónico" -#: models.py:301 +#: models.py:317 msgid "email confirmations" msgstr "confirmaciones de correos electrónicos" -#: views.py:42 +#: models.py:366 +msgid "date requested" +msgstr "" + +#: models.py:367 +msgid "date expunged" +msgstr "" + +#: models.py:370 +msgid "account deletion" +msgstr "" + +#: models.py:371 +msgid "account deletions" +msgstr "" + +#: models.py:400 +msgid "password history" +msgstr "" + +#: models.py:401 +msgid "password histories" +msgstr "" + +#: views.py:50 views.py:524 +msgid "Password successfully changed." +msgstr "La contraseña se ha cambiado con éxito." + +#: views.py:125 #, python-brace-format msgid "Confirmation email sent to {email}." msgstr "Email de confirmación enviado a {email}." -#: views.py:46 +#: views.py:129 #, python-brace-format msgid "The code {code} is invalid." msgstr "El código {code} es inválido." -#: views.py:379 +#: views.py:442 #, python-brace-format msgid "You have confirmed {email}." msgstr "Has confirmado {email}." -#: views.py:452 views.py:585 -msgid "Password successfully changed." -msgstr "La contraseña se ha cambiado con éxito." - -#: views.py:664 +#: views.py:672 msgid "Account settings updated." msgstr "Los ajustes de la cuenta actualizados." -#: views.py:748 +#: views.py:756 #, python-brace-format msgid "" "Your account is now inactive and your data will be expunged in the next " "{expunge_hours} hours." -msgstr "Tu cuenta está ahora inactiva y tus datos serán eliminados en las próximas {expunge_hours} horas." +msgstr "" +"Tu cuenta está ahora inactiva y tus datos serán eliminados en las próximas " +"{expunge_hours} horas." From 4ad25d9157bde4fbe125611aa117a3cc4af6807e Mon Sep 17 00:00:00 2001 From: operte Date: Sun, 8 Jan 2017 10:42:22 +0100 Subject: [PATCH 065/239] forms.SignupForm: move "email" before "password" Move the field "email" before "password". This way it will also appear before "password" on the signup view. Otherwise, by default, if using signup with email only, the "email" field appears after "password" and it looks awkward. --- account/forms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/account/forms.py b/account/forms.py index 4aa0cb92..efbf24a3 100644 --- a/account/forms.py +++ b/account/forms.py @@ -47,6 +47,10 @@ class SignupForm(forms.Form): widget=forms.TextInput(), required=True ) + email = forms.EmailField( + label=_("Email"), + widget=forms.TextInput(), required=True + ) password = PasswordField( label=_("Password"), strip=settings.ACCOUNT_PASSWORD_STRIP, @@ -55,10 +59,6 @@ class SignupForm(forms.Form): label=_("Password (again)"), strip=settings.ACCOUNT_PASSWORD_STRIP, ) - email = forms.EmailField( - label=_("Email"), - widget=forms.TextInput(), required=True) - code = forms.CharField( max_length=64, required=False, From f96a78ca4698f3b5c2218e680f1f103b0f416875 Mon Sep 17 00:00:00 2001 From: Dimitri Justeau Date: Mon, 23 Jan 2017 12:03:40 +1100 Subject: [PATCH 066/239] Fix a bug introduced by last commit Fix a bug introduced by porting deletion callback to hookset: In AccountDeletion model, "account_delete_expunge" and "account_delete_mark" were still called from settings, instead of using the hookset. --- account/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account/models.py b/account/models.py index 9b18c92c..e8e1888e 100644 --- a/account/models.py +++ b/account/models.py @@ -377,7 +377,7 @@ def expunge(cls, hours_ago=None): before = timezone.now() - datetime.timedelta(hours=hours_ago) count = 0 for account_deletion in cls.objects.filter(date_requested__lt=before, user__isnull=False): - settings.ACCOUNT_DELETION_EXPUNGE_CALLBACK(account_deletion) + hookset.account_delete_expunge(account_deletion) account_deletion.date_expunged = timezone.now() account_deletion.save() count += 1 @@ -388,7 +388,7 @@ def mark(cls, user): account_deletion, created = cls.objects.get_or_create(user=user) account_deletion.email = user.email account_deletion.save() - settings.ACCOUNT_DELETION_MARK_CALLBACK(account_deletion) + hookset.account_delete_mark(account_deletion) return account_deletion From 3a0d68c021b9cfd8df98607613be02ca6d192be3 Mon Sep 17 00:00:00 2001 From: Ryan Nowakowski Date: Mon, 23 Jan 2017 18:08:08 -0600 Subject: [PATCH 067/239] Add note about login_required If you use Django's login_required decorator, some stuff doesn't work correctly. Clearly indicate that developers should use our login_required decorator instead. --- docs/usage.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 7ccd109c..265c6e60 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -21,6 +21,11 @@ this app will: The rest of this document will cover how you can tweak the default behavior of django-user-accounts. +Limiting access to views +======================== + +To limit view access to logged in users, normally you would use the Django decorator ``django.contrib.auth.decorators.login_required``. Instead you should use ``account.decorators.login_required``. + Customizing the sign up process =============================== From 1fcfbf5929fdfd3e6466847759d7ae45ff3ea33d Mon Sep 17 00:00:00 2001 From: Andres Vargas Date: Thu, 30 Mar 2017 18:23:36 -0600 Subject: [PATCH 068/239] translations to spanish --- account/locale/es/LC_MESSAGES/django.mo | Bin 3185 -> 4101 bytes account/locale/es/LC_MESSAGES/django.po | 122 ++++++++++++------------ 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/account/locale/es/LC_MESSAGES/django.mo b/account/locale/es/LC_MESSAGES/django.mo index 85cc36e671827346fd0179c2b27cd9bb5b164e81..668253d8b911e139d5827c335172a12caffa1fe2 100644 GIT binary patch delta 1884 zcmZY9?`s@I7{KwTX`0mhF#Tb*t<{cITW!7bl4uiSg^FMlqSR7pi&Y$NCwGf?yK!eP zO%!`?^o72X9$G=6FZ>4*Pw^)T9*Ec%B2?&`icsq-ec=xfwHCk6-KGt8xY^IlYV&Uk9>+cO z-$H%<4DQ4~@G0Ed+j;I7Y5?!!TGp2@X%MuWL4I54c++()FXQb`N;y8tMDV#09)DkSNJt) z`W7b{f4fg)JN}5bV?UGWf_I|^_z)UAf*R@uP@ax z1~%fG$f?RA>VqF={Vz}#K7~5L52z{m1@-TKLp`Ctk<7Gr2{J;}uSyr!d9#m-R;RS{ zug%TbfWjQoMdeUrn5sn=)uMURqtkM0hxVsShT1#MhT8eofOT_xW^xm?K-D03QT4R6 z)HR=a6x+2&;RM^CjqXZMWO>Qc(f47g=*y{j*CW?7TuZ%ys{bGKlfILxY0Odeh17RS z?l(2>5)*opxKh~sv#WPy{cv1M%#8K^Xq*J5Y`t-FGhyO_yxCWA);r_vtTWa`?&zhz z%Zn`*<64xOz*Srt#?ci6UW#$xq?kCHx5E%g%jT+MZR?l%5i^W|FXKlX>wjO2vnzs5WETQ7mhFNyDmpK8~C) zJGa?|w)zs8bxAlK63*eCg9Br^!f0-^V1~yhMjD^?Y#CcVFp?|mE)@EvtWR?X5*zu7 zC8i0p&k?>gduH11E!TXxvA#DPn94RaVVrNg(ECK=T<@+;hjNoHa!K}N?1C^g!};<2 zJU8d*GW&SDPa)CRm+LR z-jJfO8n-iKy_7qNDaJbCvY;>Bv6cVb$r2XE;-cBm5>OVOS!#HFLSNRMK22gFZPt|I T#3o@U1mfTzn4pupqR#OjHgaWk delta 991 zcmX}qO=uHA6u|K@jhdusqp3}+jdp75cb0BytdLfyUlbvJPz;r#!q#l#LXxGsiC__P z(vzN+Ui2Ucg7*pvUc7kGgC8ePJ*%hc#Y08>zcl*D>~FHWGxPS%+|xh$o2$w2vLc3P zY1+sprA9Fm;zE2y1Ha);?CVf!7_+zyi?|sVa1bjP!3P+}W!!;J+xK6hjC+H7lxnI^ zboO#%4Lh+XtkeJw;uKEc1l~bO;1!PJXOxAatS%;T5(iMmU%|sz$Lsh6yYLStFusK` ztgq5^dblx)GSTU_1!N6X#1~X$4yXB_;HCXIi-+(oCh2&>D_hdaNES)aG);mJRve4gSIK`#_Fs_m|KM!4DkjNF$o|TgOUU7) z@~tgWCw0TOJ?;3~v&)WOw^x?J;f^2_Ih(3jwQ|EM+p6wV?IpKnD|t2e7@571GtA@0 zQB!A+<+EdkIc}P\n" +"POT-Creation-Date: 2017-03-30 18:22-0600\n" +"PO-Revision-Date: 2017-03-30 18:22-0600\n" +"Last-Translator: Pelana \n" "Language-Team: Spanish (http://www.transifex.com/projects/p/django-user-" "accounts/language/es/)\n" "Language: es\n" @@ -20,211 +20,213 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.9\n" -#: forms.py:45 forms.py:125 +#: account/forms.py:45 account/forms.py:125 msgid "Username" -msgstr "Nombre de usuario" +msgstr "Usuario" -#: forms.py:51 forms.py:97 +#: account/forms.py:51 account/forms.py:97 msgid "Password" msgstr "Contraseña" -#: forms.py:55 +#: account/forms.py:55 msgid "Password (again)" msgstr "Contraseña (repetir)" -#: forms.py:59 forms.py:140 forms.py:186 forms.py:215 +#: account/forms.py:59 account/forms.py:140 account/forms.py:186 +#: account/forms.py:215 msgid "Email" msgstr "Correo electrónico" -#: forms.py:70 +#: account/forms.py:70 msgid "Usernames can only contain letters, numbers and underscores." msgstr "" "Los nombres de usuario solo pueden contener letras, números y subguiones" -#: forms.py:78 +#: account/forms.py:78 msgid "This username is already taken. Please choose another." msgstr "Este nombre de usuario ya está en uso. Por favor elija otro." -#: forms.py:85 forms.py:235 +#: account/forms.py:85 account/forms.py:235 msgid "A user is registered with this email address." msgstr "Un usuario se ha registrado con esta dirección de correo electrónico." -#: forms.py:90 forms.py:180 forms.py:209 +#: account/forms.py:90 account/forms.py:180 account/forms.py:209 msgid "You must type the same password each time." msgstr "Debe escribir la misma contraseña cada vez." -#: forms.py:101 +#: account/forms.py:101 msgid "Remember Me" msgstr "Recordarme" -#: forms.py:114 +#: account/forms.py:114 msgid "This account is inactive." msgstr "Esta cuenta está inactiva." -#: forms.py:126 +#: account/forms.py:126 msgid "The username and/or password you specified are not correct." msgstr "" "El nombre de usuario y/o la contraseña que ha especificado no son correctas." -#: forms.py:141 +#: account/forms.py:141 msgid "The email address and/or password you specified are not correct." msgstr "" "La dirección de correo electrónico y/o la contraseña que ha especificado no " "son correctas." -#: forms.py:156 +#: account/forms.py:156 msgid "Current Password" msgstr "Contraseña actual" -#: forms.py:160 forms.py:198 +#: account/forms.py:160 account/forms.py:198 msgid "New Password" msgstr "Contraseña nueva" -#: forms.py:164 forms.py:202 +#: account/forms.py:164 account/forms.py:202 msgid "New Password (again)" msgstr "Contraseña nueva (repetir)" -#: forms.py:174 +#: account/forms.py:174 msgid "Please type your current password." msgstr "Por favor escriba su contraseña actual." -#: forms.py:191 +#: account/forms.py:191 msgid "Email address can not be found." msgstr "El email no pudo ser encontrado." -#: forms.py:217 +#: account/forms.py:217 msgid "Timezone" msgstr "Zona horaria" -#: forms.py:223 +#: account/forms.py:223 msgid "Language" msgstr "Idioma" -#: middleware.py:92 +#: account/middleware.py:92 msgid "Your password has expired. Please save a new password." msgstr "Tu contraseña expiró. Guarda tu Contraseña." -#: models.py:36 models.py:412 +#: account/models.py:36 account/models.py:412 msgid "user" msgstr "usuario" -#: models.py:37 +#: account/models.py:37 msgid "timezone" msgstr "zona horaria" -#: models.py:39 +#: account/models.py:39 msgid "language" msgstr "idioma" -#: models.py:140 +#: account/models.py:140 msgid "code" msgstr "código" -#: models.py:141 +#: account/models.py:141 msgid "max uses" msgstr "usos maximos" -#: models.py:142 +#: account/models.py:142 msgid "expiry" msgstr "expiró" -#: models.py:145 +#: account/models.py:145 msgid "notes" msgstr "notas" -#: models.py:146 +#: account/models.py:146 msgid "sent" msgstr "enviado" -#: models.py:147 +#: account/models.py:147 msgid "created" msgstr "creado" -#: models.py:148 +#: account/models.py:148 msgid "use count" msgstr "usos" -#: models.py:151 +#: account/models.py:151 msgid "signup code" msgstr "código " -#: models.py:152 +#: account/models.py:152 msgid "signup codes" msgstr "códigos de registro" -#: models.py:259 +#: account/models.py:259 msgid "verified" msgstr "verificado" -#: models.py:260 +#: account/models.py:260 msgid "primary" msgstr "primario" -#: models.py:265 +#: account/models.py:265 msgid "email address" msgstr "correo electrónico" -#: models.py:266 +#: account/models.py:266 msgid "email addresses" msgstr "correos electrónicos" -#: models.py:316 +#: account/models.py:316 msgid "email confirmation" msgstr "confirmación de correo electrónico" -#: models.py:317 +#: account/models.py:317 msgid "email confirmations" msgstr "confirmaciones de correos electrónicos" -#: models.py:366 +#: account/models.py:366 msgid "date requested" -msgstr "" +msgstr "fecha solicitada" -#: models.py:367 +#: account/models.py:367 msgid "date expunged" -msgstr "" +msgstr "fecha de expiración" -#: models.py:370 +#: account/models.py:370 msgid "account deletion" -msgstr "" +msgstr "cuenta borrada" -#: models.py:371 +#: account/models.py:371 msgid "account deletions" -msgstr "" +msgstr "cuentas borradas" -#: models.py:400 +#: account/models.py:400 msgid "password history" -msgstr "" +msgstr "historial de contraseña" -#: models.py:401 +#: account/models.py:401 msgid "password histories" -msgstr "" +msgstr "historico de contraseñas" -#: views.py:50 views.py:524 +#: account/views.py:50 account/views.py:524 msgid "Password successfully changed." msgstr "La contraseña se ha cambiado con éxito." -#: views.py:125 +#: account/views.py:125 #, python-brace-format msgid "Confirmation email sent to {email}." msgstr "Email de confirmación enviado a {email}." -#: views.py:129 +#: account/views.py:129 #, python-brace-format msgid "The code {code} is invalid." msgstr "El código {code} es inválido." -#: views.py:442 +#: account/views.py:442 #, python-brace-format msgid "You have confirmed {email}." msgstr "Has confirmado {email}." -#: views.py:672 +#: account/views.py:672 msgid "Account settings updated." msgstr "Los ajustes de la cuenta actualizados." -#: views.py:756 +#: account/views.py:756 #, python-brace-format msgid "" "Your account is now inactive and your data will be expunged in the next " From dca60181d78cd8ccef5c07a638e239a84326a883 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sun, 16 Apr 2017 18:21:14 -0500 Subject: [PATCH 069/239] Update CI --- .travis.yml | 14 ++++++++++++++ tox.ini | 12 +++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6b4a8e2b..e0f1f648 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,12 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" env: - DJANGO=1.8 - DJANGO=1.9 - DJANGO=1.10 + - DJANGO=1.11 - DJANGO=master matrix: exclude: @@ -17,7 +19,19 @@ matrix: - python: "3.3" env: DJANGO=1.10 - python: "3.3" + env: DJANGO=1.11 + - python: "2.7" env: DJANGO=master + - python: "3.3" + env: DJANGO=master + - python: "3.4" + env: DJANGO=master + - python: "3.6" + env: DJANGO=1.8 + - python: "3.6" + env: DJANGO=1.9 + - python: "3.6" + env: DJANGO=1.10 install: - pip install tox coveralls script: diff --git a/tox.ini b/tox.ini index e21bcd7f..7c77d849 100644 --- a/tox.ini +++ b/tox.ini @@ -2,23 +2,25 @@ ignore = E265,E501 max-line-length = 100 max-complexity = 10 -exclude = account/migrations/*,docs/* +exclude = migrations/*,docs/* [tox] envlist = - py27-{1.8,1.9,1.10,master}, + py27-{1.8,1.9,1.10,1.11}, py33-{1.8}, - py34-{1.8,1.9,1.10,master}, - py35-{1.8,1.9,1.10,master} + py34-{1.8,1.9,1.10,1.11}, + py35-{1.8,1.9,1.10,1.11,master} + py36-{1.11,master} [testenv] deps = py{27,33,34,35}: coverage==4.0.2 py32: coverage==3.7.1 - flake8==2.5.0 + flake8 == 3.3.0 1.8: Django>=1.8,<1.9 1.9: Django>=1.9,<1.10 1.10: Django>=1.10,<1.11 + 1.11: Django>=1.11,<1.12 master: https://github.com/django/django/tarball/master usedevelop = True setenv = From b23ec502b89ab70b9e8edd1868f4e9717392b7b2 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sun, 16 Apr 2017 18:21:45 -0500 Subject: [PATCH 070/239] Add missing migrations --- account/migrations/0004_auto_20170416_1821.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 account/migrations/0004_auto_20170416_1821.py diff --git a/account/migrations/0004_auto_20170416_1821.py b/account/migrations/0004_auto_20170416_1821.py new file mode 100644 index 00000000..6eb55b47 --- /dev/null +++ b/account/migrations/0004_auto_20170416_1821.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-16 18:21 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_passwordexpiry_passwordhistory'), + ] + + operations = [ + migrations.AlterModelOptions( + name='passwordhistory', + options={'verbose_name': 'password history', 'verbose_name_plural': 'password histories'}, + ), + ] From ec110fc5d7f77b4d3ab69ac818ab8d7be50a23a3 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sun, 16 Apr 2017 19:01:17 -0500 Subject: [PATCH 071/239] Update for compatiblities from 1.8 through 2.0 --- account/compat.py | 13 +++++++++++++ account/decorators.py | 3 ++- account/middleware.py | 6 +++--- account/migrations/0001_initial.py | 12 ++++++------ account/mixins.py | 3 ++- account/models.py | 20 ++++++++++---------- account/tests/test_password.py | 2 +- account/tests/test_views.py | 2 +- account/utils.py | 10 +++++----- account/views.py | 18 +++++++++--------- runtests.py | 19 +++++++++++++------ setup.py | 1 + 12 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 account/compat.py diff --git a/account/compat.py b/account/compat.py new file mode 100644 index 00000000..94704db1 --- /dev/null +++ b/account/compat.py @@ -0,0 +1,13 @@ +import django + +try: + from django.core.urlresolvers import resolve, reverse, NoReverseMatch +except ImportError: + from django.urls import resolve, reverse, NoReverseMatch # noqa + + +def is_authenticated(user): + if django.VERSION >= (1, 10): + return user.is_authenticated + else: + return user.is_authenticated() diff --git a/account/decorators.py b/account/decorators.py index 46ddcdd7..13ac74c3 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -5,6 +5,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.decorators import available_attrs +from account.compat import is_authenticated from account.utils import handle_redirect_to_login @@ -16,7 +17,7 @@ def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url def decorator(view_func): @functools.wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): - if request.user.is_authenticated(): + if is_authenticated(request.user): return view_func(request, *args, **kwargs) return handle_redirect_to_login( request, diff --git a/account/middleware.py b/account/middleware.py index e54decd8..f42dde18 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -9,13 +9,13 @@ from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME -from django.core.urlresolvers import resolve, reverse from django.http import HttpResponseRedirect, QueryDict from django.utils import translation, timezone from django.utils.cache import patch_vary_headers from django.utils.translation import ugettext_lazy as _ from account import signals +from account.compat import resolve, reverse, is_authenticated from account.conf import settings from account.models import Account from account.utils import check_password_expired @@ -37,7 +37,7 @@ class LocaleMiddleware(BaseMiddleware): """ def get_language_for_user(self, request): - if request.user.is_authenticated(): + if is_authenticated(request.user): try: account = Account.objects.get(user=request.user) return account.language @@ -76,7 +76,7 @@ def process_request(self, request): class ExpiredPasswordMiddleware(BaseMiddleware): def process_request(self, request): - if request.user.is_authenticated() and not request.user.is_staff: + if is_authenticated(request.user) and not request.user.is_staff: next_url = resolve(request.path).url_name # Authenticated users must be allowed to access # "change password" page and "log out" page. diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py index 7205e1c9..90682dcb 100644 --- a/account/migrations/0001_initial.py +++ b/account/migrations/0001_initial.py @@ -21,7 +21,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), ('timezone', account.fields.TimeZoneField(default='', verbose_name='timezone', max_length=100, choices=[(b'Africa/Abidjan', b'Africa/Abidjan'), (b'Africa/Accra', b'Africa/Accra'), (b'Africa/Addis_Ababa', b'Africa/Addis_Ababa'), (b'Africa/Algiers', b'Africa/Algiers'), (b'Africa/Asmara', b'Africa/Asmara'), (b'Africa/Asmera', b'Africa/Asmera'), (b'Africa/Bamako', b'Africa/Bamako'), (b'Africa/Bangui', b'Africa/Bangui'), (b'Africa/Banjul', b'Africa/Banjul'), (b'Africa/Bissau', b'Africa/Bissau'), (b'Africa/Blantyre', b'Africa/Blantyre'), (b'Africa/Brazzaville', b'Africa/Brazzaville'), (b'Africa/Bujumbura', b'Africa/Bujumbura'), (b'Africa/Cairo', b'Africa/Cairo'), (b'Africa/Casablanca', b'Africa/Casablanca'), (b'Africa/Ceuta', b'Africa/Ceuta'), (b'Africa/Conakry', b'Africa/Conakry'), (b'Africa/Dakar', b'Africa/Dakar'), (b'Africa/Dar_es_Salaam', b'Africa/Dar_es_Salaam'), (b'Africa/Djibouti', b'Africa/Djibouti'), (b'Africa/Douala', b'Africa/Douala'), (b'Africa/El_Aaiun', b'Africa/El_Aaiun'), (b'Africa/Freetown', b'Africa/Freetown'), (b'Africa/Gaborone', b'Africa/Gaborone'), (b'Africa/Harare', b'Africa/Harare'), (b'Africa/Johannesburg', b'Africa/Johannesburg'), (b'Africa/Juba', b'Africa/Juba'), (b'Africa/Kampala', b'Africa/Kampala'), (b'Africa/Khartoum', b'Africa/Khartoum'), (b'Africa/Kigali', b'Africa/Kigali'), (b'Africa/Kinshasa', b'Africa/Kinshasa'), (b'Africa/Lagos', b'Africa/Lagos'), (b'Africa/Libreville', b'Africa/Libreville'), (b'Africa/Lome', b'Africa/Lome'), (b'Africa/Luanda', b'Africa/Luanda'), (b'Africa/Lubumbashi', b'Africa/Lubumbashi'), (b'Africa/Lusaka', b'Africa/Lusaka'), (b'Africa/Malabo', b'Africa/Malabo'), (b'Africa/Maputo', b'Africa/Maputo'), (b'Africa/Maseru', b'Africa/Maseru'), (b'Africa/Mbabane', b'Africa/Mbabane'), (b'Africa/Mogadishu', b'Africa/Mogadishu'), (b'Africa/Monrovia', b'Africa/Monrovia'), (b'Africa/Nairobi', b'Africa/Nairobi'), (b'Africa/Ndjamena', b'Africa/Ndjamena'), (b'Africa/Niamey', b'Africa/Niamey'), (b'Africa/Nouakchott', b'Africa/Nouakchott'), (b'Africa/Ouagadougou', b'Africa/Ouagadougou'), (b'Africa/Porto-Novo', b'Africa/Porto-Novo'), (b'Africa/Sao_Tome', b'Africa/Sao_Tome'), (b'Africa/Timbuktu', b'Africa/Timbuktu'), (b'Africa/Tripoli', b'Africa/Tripoli'), (b'Africa/Tunis', b'Africa/Tunis'), (b'Africa/Windhoek', b'Africa/Windhoek'), (b'America/Adak', b'America/Adak'), (b'America/Anchorage', b'America/Anchorage'), (b'America/Anguilla', b'America/Anguilla'), (b'America/Antigua', b'America/Antigua'), (b'America/Araguaina', b'America/Araguaina'), (b'America/Argentina/Buenos_Aires', b'America/Argentina/Buenos_Aires'), (b'America/Argentina/Catamarca', b'America/Argentina/Catamarca'), (b'America/Argentina/ComodRivadavia', b'America/Argentina/ComodRivadavia'), (b'America/Argentina/Cordoba', b'America/Argentina/Cordoba'), (b'America/Argentina/Jujuy', b'America/Argentina/Jujuy'), (b'America/Argentina/La_Rioja', b'America/Argentina/La_Rioja'), (b'America/Argentina/Mendoza', b'America/Argentina/Mendoza'), (b'America/Argentina/Rio_Gallegos', b'America/Argentina/Rio_Gallegos'), (b'America/Argentina/Salta', b'America/Argentina/Salta'), (b'America/Argentina/San_Juan', b'America/Argentina/San_Juan'), (b'America/Argentina/San_Luis', b'America/Argentina/San_Luis'), (b'America/Argentina/Tucuman', b'America/Argentina/Tucuman'), (b'America/Argentina/Ushuaia', b'America/Argentina/Ushuaia'), (b'America/Aruba', b'America/Aruba'), (b'America/Asuncion', b'America/Asuncion'), (b'America/Atikokan', b'America/Atikokan'), (b'America/Atka', b'America/Atka'), (b'America/Bahia', b'America/Bahia'), (b'America/Bahia_Banderas', b'America/Bahia_Banderas'), (b'America/Barbados', b'America/Barbados'), (b'America/Belem', b'America/Belem'), (b'America/Belize', b'America/Belize'), (b'America/Blanc-Sablon', b'America/Blanc-Sablon'), (b'America/Boa_Vista', b'America/Boa_Vista'), (b'America/Bogota', b'America/Bogota'), (b'America/Boise', b'America/Boise'), (b'America/Buenos_Aires', b'America/Buenos_Aires'), (b'America/Cambridge_Bay', b'America/Cambridge_Bay'), (b'America/Campo_Grande', b'America/Campo_Grande'), (b'America/Cancun', b'America/Cancun'), (b'America/Caracas', b'America/Caracas'), (b'America/Catamarca', b'America/Catamarca'), (b'America/Cayenne', b'America/Cayenne'), (b'America/Cayman', b'America/Cayman'), (b'America/Chicago', b'America/Chicago'), (b'America/Chihuahua', b'America/Chihuahua'), (b'America/Coral_Harbour', b'America/Coral_Harbour'), (b'America/Cordoba', b'America/Cordoba'), (b'America/Costa_Rica', b'America/Costa_Rica'), (b'America/Creston', b'America/Creston'), (b'America/Cuiaba', b'America/Cuiaba'), (b'America/Curacao', b'America/Curacao'), (b'America/Danmarkshavn', b'America/Danmarkshavn'), (b'America/Dawson', b'America/Dawson'), (b'America/Dawson_Creek', b'America/Dawson_Creek'), (b'America/Denver', b'America/Denver'), (b'America/Detroit', b'America/Detroit'), (b'America/Dominica', b'America/Dominica'), (b'America/Edmonton', b'America/Edmonton'), (b'America/Eirunepe', b'America/Eirunepe'), (b'America/El_Salvador', b'America/El_Salvador'), (b'America/Ensenada', b'America/Ensenada'), (b'America/Fort_Wayne', b'America/Fort_Wayne'), (b'America/Fortaleza', b'America/Fortaleza'), (b'America/Glace_Bay', b'America/Glace_Bay'), (b'America/Godthab', b'America/Godthab'), (b'America/Goose_Bay', b'America/Goose_Bay'), (b'America/Grand_Turk', b'America/Grand_Turk'), (b'America/Grenada', b'America/Grenada'), (b'America/Guadeloupe', b'America/Guadeloupe'), (b'America/Guatemala', b'America/Guatemala'), (b'America/Guayaquil', b'America/Guayaquil'), (b'America/Guyana', b'America/Guyana'), (b'America/Halifax', b'America/Halifax'), (b'America/Havana', b'America/Havana'), (b'America/Hermosillo', b'America/Hermosillo'), (b'America/Indiana/Indianapolis', b'America/Indiana/Indianapolis'), (b'America/Indiana/Knox', b'America/Indiana/Knox'), (b'America/Indiana/Marengo', b'America/Indiana/Marengo'), (b'America/Indiana/Petersburg', b'America/Indiana/Petersburg'), (b'America/Indiana/Tell_City', b'America/Indiana/Tell_City'), (b'America/Indiana/Vevay', b'America/Indiana/Vevay'), (b'America/Indiana/Vincennes', b'America/Indiana/Vincennes'), (b'America/Indiana/Winamac', b'America/Indiana/Winamac'), (b'America/Indianapolis', b'America/Indianapolis'), (b'America/Inuvik', b'America/Inuvik'), (b'America/Iqaluit', b'America/Iqaluit'), (b'America/Jamaica', b'America/Jamaica'), (b'America/Jujuy', b'America/Jujuy'), (b'America/Juneau', b'America/Juneau'), (b'America/Kentucky/Louisville', b'America/Kentucky/Louisville'), (b'America/Kentucky/Monticello', b'America/Kentucky/Monticello'), (b'America/Knox_IN', b'America/Knox_IN'), (b'America/Kralendijk', b'America/Kralendijk'), (b'America/La_Paz', b'America/La_Paz'), (b'America/Lima', b'America/Lima'), (b'America/Los_Angeles', b'America/Los_Angeles'), (b'America/Louisville', b'America/Louisville'), (b'America/Lower_Princes', b'America/Lower_Princes'), (b'America/Maceio', b'America/Maceio'), (b'America/Managua', b'America/Managua'), (b'America/Manaus', b'America/Manaus'), (b'America/Marigot', b'America/Marigot'), (b'America/Martinique', b'America/Martinique'), (b'America/Matamoros', b'America/Matamoros'), (b'America/Mazatlan', b'America/Mazatlan'), (b'America/Mendoza', b'America/Mendoza'), (b'America/Menominee', b'America/Menominee'), (b'America/Merida', b'America/Merida'), (b'America/Metlakatla', b'America/Metlakatla'), (b'America/Mexico_City', b'America/Mexico_City'), (b'America/Miquelon', b'America/Miquelon'), (b'America/Moncton', b'America/Moncton'), (b'America/Monterrey', b'America/Monterrey'), (b'America/Montevideo', b'America/Montevideo'), (b'America/Montreal', b'America/Montreal'), (b'America/Montserrat', b'America/Montserrat'), (b'America/Nassau', b'America/Nassau'), (b'America/New_York', b'America/New_York'), (b'America/Nipigon', b'America/Nipigon'), (b'America/Nome', b'America/Nome'), (b'America/Noronha', b'America/Noronha'), (b'America/North_Dakota/Beulah', b'America/North_Dakota/Beulah'), (b'America/North_Dakota/Center', b'America/North_Dakota/Center'), (b'America/North_Dakota/New_Salem', b'America/North_Dakota/New_Salem'), (b'America/Ojinaga', b'America/Ojinaga'), (b'America/Panama', b'America/Panama'), (b'America/Pangnirtung', b'America/Pangnirtung'), (b'America/Paramaribo', b'America/Paramaribo'), (b'America/Phoenix', b'America/Phoenix'), (b'America/Port-au-Prince', b'America/Port-au-Prince'), (b'America/Port_of_Spain', b'America/Port_of_Spain'), (b'America/Porto_Acre', b'America/Porto_Acre'), (b'America/Porto_Velho', b'America/Porto_Velho'), (b'America/Puerto_Rico', b'America/Puerto_Rico'), (b'America/Rainy_River', b'America/Rainy_River'), (b'America/Rankin_Inlet', b'America/Rankin_Inlet'), (b'America/Recife', b'America/Recife'), (b'America/Regina', b'America/Regina'), (b'America/Resolute', b'America/Resolute'), (b'America/Rio_Branco', b'America/Rio_Branco'), (b'America/Rosario', b'America/Rosario'), (b'America/Santa_Isabel', b'America/Santa_Isabel'), (b'America/Santarem', b'America/Santarem'), (b'America/Santiago', b'America/Santiago'), (b'America/Santo_Domingo', b'America/Santo_Domingo'), (b'America/Sao_Paulo', b'America/Sao_Paulo'), (b'America/Scoresbysund', b'America/Scoresbysund'), (b'America/Shiprock', b'America/Shiprock'), (b'America/Sitka', b'America/Sitka'), (b'America/St_Barthelemy', b'America/St_Barthelemy'), (b'America/St_Johns', b'America/St_Johns'), (b'America/St_Kitts', b'America/St_Kitts'), (b'America/St_Lucia', b'America/St_Lucia'), (b'America/St_Thomas', b'America/St_Thomas'), (b'America/St_Vincent', b'America/St_Vincent'), (b'America/Swift_Current', b'America/Swift_Current'), (b'America/Tegucigalpa', b'America/Tegucigalpa'), (b'America/Thule', b'America/Thule'), (b'America/Thunder_Bay', b'America/Thunder_Bay'), (b'America/Tijuana', b'America/Tijuana'), (b'America/Toronto', b'America/Toronto'), (b'America/Tortola', b'America/Tortola'), (b'America/Vancouver', b'America/Vancouver'), (b'America/Virgin', b'America/Virgin'), (b'America/Whitehorse', b'America/Whitehorse'), (b'America/Winnipeg', b'America/Winnipeg'), (b'America/Yakutat', b'America/Yakutat'), (b'America/Yellowknife', b'America/Yellowknife'), (b'Antarctica/Casey', b'Antarctica/Casey'), (b'Antarctica/Davis', b'Antarctica/Davis'), (b'Antarctica/DumontDUrville', b'Antarctica/DumontDUrville'), (b'Antarctica/Macquarie', b'Antarctica/Macquarie'), (b'Antarctica/Mawson', b'Antarctica/Mawson'), (b'Antarctica/McMurdo', b'Antarctica/McMurdo'), (b'Antarctica/Palmer', b'Antarctica/Palmer'), (b'Antarctica/Rothera', b'Antarctica/Rothera'), (b'Antarctica/South_Pole', b'Antarctica/South_Pole'), (b'Antarctica/Syowa', b'Antarctica/Syowa'), (b'Antarctica/Troll', b'Antarctica/Troll'), (b'Antarctica/Vostok', b'Antarctica/Vostok'), (b'Arctic/Longyearbyen', b'Arctic/Longyearbyen'), (b'Asia/Aden', b'Asia/Aden'), (b'Asia/Almaty', b'Asia/Almaty'), (b'Asia/Amman', b'Asia/Amman'), (b'Asia/Anadyr', b'Asia/Anadyr'), (b'Asia/Aqtau', b'Asia/Aqtau'), (b'Asia/Aqtobe', b'Asia/Aqtobe'), (b'Asia/Ashgabat', b'Asia/Ashgabat'), (b'Asia/Ashkhabad', b'Asia/Ashkhabad'), (b'Asia/Baghdad', b'Asia/Baghdad'), (b'Asia/Bahrain', b'Asia/Bahrain'), (b'Asia/Baku', b'Asia/Baku'), (b'Asia/Bangkok', b'Asia/Bangkok'), (b'Asia/Beirut', b'Asia/Beirut'), (b'Asia/Bishkek', b'Asia/Bishkek'), (b'Asia/Brunei', b'Asia/Brunei'), (b'Asia/Calcutta', b'Asia/Calcutta'), (b'Asia/Chita', b'Asia/Chita'), (b'Asia/Choibalsan', b'Asia/Choibalsan'), (b'Asia/Chongqing', b'Asia/Chongqing'), (b'Asia/Chungking', b'Asia/Chungking'), (b'Asia/Colombo', b'Asia/Colombo'), (b'Asia/Dacca', b'Asia/Dacca'), (b'Asia/Damascus', b'Asia/Damascus'), (b'Asia/Dhaka', b'Asia/Dhaka'), (b'Asia/Dili', b'Asia/Dili'), (b'Asia/Dubai', b'Asia/Dubai'), (b'Asia/Dushanbe', b'Asia/Dushanbe'), (b'Asia/Gaza', b'Asia/Gaza'), (b'Asia/Harbin', b'Asia/Harbin'), (b'Asia/Hebron', b'Asia/Hebron'), (b'Asia/Ho_Chi_Minh', b'Asia/Ho_Chi_Minh'), (b'Asia/Hong_Kong', b'Asia/Hong_Kong'), (b'Asia/Hovd', b'Asia/Hovd'), (b'Asia/Irkutsk', b'Asia/Irkutsk'), (b'Asia/Istanbul', b'Asia/Istanbul'), (b'Asia/Jakarta', b'Asia/Jakarta'), (b'Asia/Jayapura', b'Asia/Jayapura'), (b'Asia/Jerusalem', b'Asia/Jerusalem'), (b'Asia/Kabul', b'Asia/Kabul'), (b'Asia/Kamchatka', b'Asia/Kamchatka'), (b'Asia/Karachi', b'Asia/Karachi'), (b'Asia/Kashgar', b'Asia/Kashgar'), (b'Asia/Kathmandu', b'Asia/Kathmandu'), (b'Asia/Katmandu', b'Asia/Katmandu'), (b'Asia/Khandyga', b'Asia/Khandyga'), (b'Asia/Kolkata', b'Asia/Kolkata'), (b'Asia/Krasnoyarsk', b'Asia/Krasnoyarsk'), (b'Asia/Kuala_Lumpur', b'Asia/Kuala_Lumpur'), (b'Asia/Kuching', b'Asia/Kuching'), (b'Asia/Kuwait', b'Asia/Kuwait'), (b'Asia/Macao', b'Asia/Macao'), (b'Asia/Macau', b'Asia/Macau'), (b'Asia/Magadan', b'Asia/Magadan'), (b'Asia/Makassar', b'Asia/Makassar'), (b'Asia/Manila', b'Asia/Manila'), (b'Asia/Muscat', b'Asia/Muscat'), (b'Asia/Nicosia', b'Asia/Nicosia'), (b'Asia/Novokuznetsk', b'Asia/Novokuznetsk'), (b'Asia/Novosibirsk', b'Asia/Novosibirsk'), (b'Asia/Omsk', b'Asia/Omsk'), (b'Asia/Oral', b'Asia/Oral'), (b'Asia/Phnom_Penh', b'Asia/Phnom_Penh'), (b'Asia/Pontianak', b'Asia/Pontianak'), (b'Asia/Pyongyang', b'Asia/Pyongyang'), (b'Asia/Qatar', b'Asia/Qatar'), (b'Asia/Qyzylorda', b'Asia/Qyzylorda'), (b'Asia/Rangoon', b'Asia/Rangoon'), (b'Asia/Riyadh', b'Asia/Riyadh'), (b'Asia/Saigon', b'Asia/Saigon'), (b'Asia/Sakhalin', b'Asia/Sakhalin'), (b'Asia/Samarkand', b'Asia/Samarkand'), (b'Asia/Seoul', b'Asia/Seoul'), (b'Asia/Shanghai', b'Asia/Shanghai'), (b'Asia/Singapore', b'Asia/Singapore'), (b'Asia/Srednekolymsk', b'Asia/Srednekolymsk'), (b'Asia/Taipei', b'Asia/Taipei'), (b'Asia/Tashkent', b'Asia/Tashkent'), (b'Asia/Tbilisi', b'Asia/Tbilisi'), (b'Asia/Tehran', b'Asia/Tehran'), (b'Asia/Tel_Aviv', b'Asia/Tel_Aviv'), (b'Asia/Thimbu', b'Asia/Thimbu'), (b'Asia/Thimphu', b'Asia/Thimphu'), (b'Asia/Tokyo', b'Asia/Tokyo'), (b'Asia/Ujung_Pandang', b'Asia/Ujung_Pandang'), (b'Asia/Ulaanbaatar', b'Asia/Ulaanbaatar'), (b'Asia/Ulan_Bator', b'Asia/Ulan_Bator'), (b'Asia/Urumqi', b'Asia/Urumqi'), (b'Asia/Ust-Nera', b'Asia/Ust-Nera'), (b'Asia/Vientiane', b'Asia/Vientiane'), (b'Asia/Vladivostok', b'Asia/Vladivostok'), (b'Asia/Yakutsk', b'Asia/Yakutsk'), (b'Asia/Yekaterinburg', b'Asia/Yekaterinburg'), (b'Asia/Yerevan', b'Asia/Yerevan'), (b'Atlantic/Azores', b'Atlantic/Azores'), (b'Atlantic/Bermuda', b'Atlantic/Bermuda'), (b'Atlantic/Canary', b'Atlantic/Canary'), (b'Atlantic/Cape_Verde', b'Atlantic/Cape_Verde'), (b'Atlantic/Faeroe', b'Atlantic/Faeroe'), (b'Atlantic/Faroe', b'Atlantic/Faroe'), (b'Atlantic/Jan_Mayen', b'Atlantic/Jan_Mayen'), (b'Atlantic/Madeira', b'Atlantic/Madeira'), (b'Atlantic/Reykjavik', b'Atlantic/Reykjavik'), (b'Atlantic/South_Georgia', b'Atlantic/South_Georgia'), (b'Atlantic/St_Helena', b'Atlantic/St_Helena'), (b'Atlantic/Stanley', b'Atlantic/Stanley'), (b'Australia/ACT', b'Australia/ACT'), (b'Australia/Adelaide', b'Australia/Adelaide'), (b'Australia/Brisbane', b'Australia/Brisbane'), (b'Australia/Broken_Hill', b'Australia/Broken_Hill'), (b'Australia/Canberra', b'Australia/Canberra'), (b'Australia/Currie', b'Australia/Currie'), (b'Australia/Darwin', b'Australia/Darwin'), (b'Australia/Eucla', b'Australia/Eucla'), (b'Australia/Hobart', b'Australia/Hobart'), (b'Australia/LHI', b'Australia/LHI'), (b'Australia/Lindeman', b'Australia/Lindeman'), (b'Australia/Lord_Howe', b'Australia/Lord_Howe'), (b'Australia/Melbourne', b'Australia/Melbourne'), (b'Australia/NSW', b'Australia/NSW'), (b'Australia/North', b'Australia/North'), (b'Australia/Perth', b'Australia/Perth'), (b'Australia/Queensland', b'Australia/Queensland'), (b'Australia/South', b'Australia/South'), (b'Australia/Sydney', b'Australia/Sydney'), (b'Australia/Tasmania', b'Australia/Tasmania'), (b'Australia/Victoria', b'Australia/Victoria'), (b'Australia/West', b'Australia/West'), (b'Australia/Yancowinna', b'Australia/Yancowinna'), (b'Brazil/Acre', b'Brazil/Acre'), (b'Brazil/DeNoronha', b'Brazil/DeNoronha'), (b'Brazil/East', b'Brazil/East'), (b'Brazil/West', b'Brazil/West'), (b'CET', b'CET'), (b'CST6CDT', b'CST6CDT'), (b'Canada/Atlantic', b'Canada/Atlantic'), (b'Canada/Central', b'Canada/Central'), (b'Canada/East-Saskatchewan', b'Canada/East-Saskatchewan'), (b'Canada/Eastern', b'Canada/Eastern'), (b'Canada/Mountain', b'Canada/Mountain'), (b'Canada/Newfoundland', b'Canada/Newfoundland'), (b'Canada/Pacific', b'Canada/Pacific'), (b'Canada/Saskatchewan', b'Canada/Saskatchewan'), (b'Canada/Yukon', b'Canada/Yukon'), (b'Chile/Continental', b'Chile/Continental'), (b'Chile/EasterIsland', b'Chile/EasterIsland'), (b'Cuba', b'Cuba'), (b'EET', b'EET'), (b'EST', b'EST'), (b'EST5EDT', b'EST5EDT'), (b'Egypt', b'Egypt'), (b'Eire', b'Eire'), (b'Etc/GMT', b'Etc/GMT'), (b'Etc/GMT+0', b'Etc/GMT+0'), (b'Etc/GMT+1', b'Etc/GMT+1'), (b'Etc/GMT+10', b'Etc/GMT+10'), (b'Etc/GMT+11', b'Etc/GMT+11'), (b'Etc/GMT+12', b'Etc/GMT+12'), (b'Etc/GMT+2', b'Etc/GMT+2'), (b'Etc/GMT+3', b'Etc/GMT+3'), (b'Etc/GMT+4', b'Etc/GMT+4'), (b'Etc/GMT+5', b'Etc/GMT+5'), (b'Etc/GMT+6', b'Etc/GMT+6'), (b'Etc/GMT+7', b'Etc/GMT+7'), (b'Etc/GMT+8', b'Etc/GMT+8'), (b'Etc/GMT+9', b'Etc/GMT+9'), (b'Etc/GMT-0', b'Etc/GMT-0'), (b'Etc/GMT-1', b'Etc/GMT-1'), (b'Etc/GMT-10', b'Etc/GMT-10'), (b'Etc/GMT-11', b'Etc/GMT-11'), (b'Etc/GMT-12', b'Etc/GMT-12'), (b'Etc/GMT-13', b'Etc/GMT-13'), (b'Etc/GMT-14', b'Etc/GMT-14'), (b'Etc/GMT-2', b'Etc/GMT-2'), (b'Etc/GMT-3', b'Etc/GMT-3'), (b'Etc/GMT-4', b'Etc/GMT-4'), (b'Etc/GMT-5', b'Etc/GMT-5'), (b'Etc/GMT-6', b'Etc/GMT-6'), (b'Etc/GMT-7', b'Etc/GMT-7'), (b'Etc/GMT-8', b'Etc/GMT-8'), (b'Etc/GMT-9', b'Etc/GMT-9'), (b'Etc/GMT0', b'Etc/GMT0'), (b'Etc/Greenwich', b'Etc/Greenwich'), (b'Etc/UCT', b'Etc/UCT'), (b'Etc/UTC', b'Etc/UTC'), (b'Etc/Universal', b'Etc/Universal'), (b'Etc/Zulu', b'Etc/Zulu'), (b'Europe/Amsterdam', b'Europe/Amsterdam'), (b'Europe/Andorra', b'Europe/Andorra'), (b'Europe/Athens', b'Europe/Athens'), (b'Europe/Belfast', b'Europe/Belfast'), (b'Europe/Belgrade', b'Europe/Belgrade'), (b'Europe/Berlin', b'Europe/Berlin'), (b'Europe/Bratislava', b'Europe/Bratislava'), (b'Europe/Brussels', b'Europe/Brussels'), (b'Europe/Bucharest', b'Europe/Bucharest'), (b'Europe/Budapest', b'Europe/Budapest'), (b'Europe/Busingen', b'Europe/Busingen'), (b'Europe/Chisinau', b'Europe/Chisinau'), (b'Europe/Copenhagen', b'Europe/Copenhagen'), (b'Europe/Dublin', b'Europe/Dublin'), (b'Europe/Gibraltar', b'Europe/Gibraltar'), (b'Europe/Guernsey', b'Europe/Guernsey'), (b'Europe/Helsinki', b'Europe/Helsinki'), (b'Europe/Isle_of_Man', b'Europe/Isle_of_Man'), (b'Europe/Istanbul', b'Europe/Istanbul'), (b'Europe/Jersey', b'Europe/Jersey'), (b'Europe/Kaliningrad', b'Europe/Kaliningrad'), (b'Europe/Kiev', b'Europe/Kiev'), (b'Europe/Lisbon', b'Europe/Lisbon'), (b'Europe/Ljubljana', b'Europe/Ljubljana'), (b'Europe/London', b'Europe/London'), (b'Europe/Luxembourg', b'Europe/Luxembourg'), (b'Europe/Madrid', b'Europe/Madrid'), (b'Europe/Malta', b'Europe/Malta'), (b'Europe/Mariehamn', b'Europe/Mariehamn'), (b'Europe/Minsk', b'Europe/Minsk'), (b'Europe/Monaco', b'Europe/Monaco'), (b'Europe/Moscow', b'Europe/Moscow'), (b'Europe/Nicosia', b'Europe/Nicosia'), (b'Europe/Oslo', b'Europe/Oslo'), (b'Europe/Paris', b'Europe/Paris'), (b'Europe/Podgorica', b'Europe/Podgorica'), (b'Europe/Prague', b'Europe/Prague'), (b'Europe/Riga', b'Europe/Riga'), (b'Europe/Rome', b'Europe/Rome'), (b'Europe/Samara', b'Europe/Samara'), (b'Europe/San_Marino', b'Europe/San_Marino'), (b'Europe/Sarajevo', b'Europe/Sarajevo'), (b'Europe/Simferopol', b'Europe/Simferopol'), (b'Europe/Skopje', b'Europe/Skopje'), (b'Europe/Sofia', b'Europe/Sofia'), (b'Europe/Stockholm', b'Europe/Stockholm'), (b'Europe/Tallinn', b'Europe/Tallinn'), (b'Europe/Tirane', b'Europe/Tirane'), (b'Europe/Tiraspol', b'Europe/Tiraspol'), (b'Europe/Uzhgorod', b'Europe/Uzhgorod'), (b'Europe/Vaduz', b'Europe/Vaduz'), (b'Europe/Vatican', b'Europe/Vatican'), (b'Europe/Vienna', b'Europe/Vienna'), (b'Europe/Vilnius', b'Europe/Vilnius'), (b'Europe/Volgograd', b'Europe/Volgograd'), (b'Europe/Warsaw', b'Europe/Warsaw'), (b'Europe/Zagreb', b'Europe/Zagreb'), (b'Europe/Zaporozhye', b'Europe/Zaporozhye'), (b'Europe/Zurich', b'Europe/Zurich'), (b'GB', b'GB'), (b'GB-Eire', b'GB-Eire'), (b'GMT', b'GMT'), (b'GMT+0', b'GMT+0'), (b'GMT-0', b'GMT-0'), (b'GMT0', b'GMT0'), (b'Greenwich', b'Greenwich'), (b'HST', b'HST'), (b'Hongkong', b'Hongkong'), (b'Iceland', b'Iceland'), (b'Indian/Antananarivo', b'Indian/Antananarivo'), (b'Indian/Chagos', b'Indian/Chagos'), (b'Indian/Christmas', b'Indian/Christmas'), (b'Indian/Cocos', b'Indian/Cocos'), (b'Indian/Comoro', b'Indian/Comoro'), (b'Indian/Kerguelen', b'Indian/Kerguelen'), (b'Indian/Mahe', b'Indian/Mahe'), (b'Indian/Maldives', b'Indian/Maldives'), (b'Indian/Mauritius', b'Indian/Mauritius'), (b'Indian/Mayotte', b'Indian/Mayotte'), (b'Indian/Reunion', b'Indian/Reunion'), (b'Iran', b'Iran'), (b'Israel', b'Israel'), (b'Jamaica', b'Jamaica'), (b'Japan', b'Japan'), (b'Kwajalein', b'Kwajalein'), (b'Libya', b'Libya'), (b'MET', b'MET'), (b'MST', b'MST'), (b'MST7MDT', b'MST7MDT'), (b'Mexico/BajaNorte', b'Mexico/BajaNorte'), (b'Mexico/BajaSur', b'Mexico/BajaSur'), (b'Mexico/General', b'Mexico/General'), (b'NZ', b'NZ'), (b'NZ-CHAT', b'NZ-CHAT'), (b'Navajo', b'Navajo'), (b'PRC', b'PRC'), (b'PST8PDT', b'PST8PDT'), (b'Pacific/Apia', b'Pacific/Apia'), (b'Pacific/Auckland', b'Pacific/Auckland'), (b'Pacific/Bougainville', b'Pacific/Bougainville'), (b'Pacific/Chatham', b'Pacific/Chatham'), (b'Pacific/Chuuk', b'Pacific/Chuuk'), (b'Pacific/Easter', b'Pacific/Easter'), (b'Pacific/Efate', b'Pacific/Efate'), (b'Pacific/Enderbury', b'Pacific/Enderbury'), (b'Pacific/Fakaofo', b'Pacific/Fakaofo'), (b'Pacific/Fiji', b'Pacific/Fiji'), (b'Pacific/Funafuti', b'Pacific/Funafuti'), (b'Pacific/Galapagos', b'Pacific/Galapagos'), (b'Pacific/Gambier', b'Pacific/Gambier'), (b'Pacific/Guadalcanal', b'Pacific/Guadalcanal'), (b'Pacific/Guam', b'Pacific/Guam'), (b'Pacific/Honolulu', b'Pacific/Honolulu'), (b'Pacific/Johnston', b'Pacific/Johnston'), (b'Pacific/Kiritimati', b'Pacific/Kiritimati'), (b'Pacific/Kosrae', b'Pacific/Kosrae'), (b'Pacific/Kwajalein', b'Pacific/Kwajalein'), (b'Pacific/Majuro', b'Pacific/Majuro'), (b'Pacific/Marquesas', b'Pacific/Marquesas'), (b'Pacific/Midway', b'Pacific/Midway'), (b'Pacific/Nauru', b'Pacific/Nauru'), (b'Pacific/Niue', b'Pacific/Niue'), (b'Pacific/Norfolk', b'Pacific/Norfolk'), (b'Pacific/Noumea', b'Pacific/Noumea'), (b'Pacific/Pago_Pago', b'Pacific/Pago_Pago'), (b'Pacific/Palau', b'Pacific/Palau'), (b'Pacific/Pitcairn', b'Pacific/Pitcairn'), (b'Pacific/Pohnpei', b'Pacific/Pohnpei'), (b'Pacific/Ponape', b'Pacific/Ponape'), (b'Pacific/Port_Moresby', b'Pacific/Port_Moresby'), (b'Pacific/Rarotonga', b'Pacific/Rarotonga'), (b'Pacific/Saipan', b'Pacific/Saipan'), (b'Pacific/Samoa', b'Pacific/Samoa'), (b'Pacific/Tahiti', b'Pacific/Tahiti'), (b'Pacific/Tarawa', b'Pacific/Tarawa'), (b'Pacific/Tongatapu', b'Pacific/Tongatapu'), (b'Pacific/Truk', b'Pacific/Truk'), (b'Pacific/Wake', b'Pacific/Wake'), (b'Pacific/Wallis', b'Pacific/Wallis'), (b'Pacific/Yap', b'Pacific/Yap'), (b'Poland', b'Poland'), (b'Portugal', b'Portugal'), (b'ROC', b'ROC'), (b'ROK', b'ROK'), (b'Singapore', b'Singapore'), (b'Turkey', b'Turkey'), (b'UCT', b'UCT'), (b'US/Alaska', b'US/Alaska'), (b'US/Aleutian', b'US/Aleutian'), (b'US/Arizona', b'US/Arizona'), (b'US/Central', b'US/Central'), (b'US/East-Indiana', b'US/East-Indiana'), (b'US/Eastern', b'US/Eastern'), (b'US/Hawaii', b'US/Hawaii'), (b'US/Indiana-Starke', b'US/Indiana-Starke'), (b'US/Michigan', b'US/Michigan'), (b'US/Mountain', b'US/Mountain'), (b'US/Pacific', b'US/Pacific'), (b'US/Pacific-New', b'US/Pacific-New'), (b'US/Samoa', b'US/Samoa'), (b'UTC', b'UTC'), (b'Universal', b'Universal'), (b'W-SU', b'W-SU'), (b'WET', b'WET'), (b'Zulu', b'Zulu')], blank=True)), ('language', models.CharField(default=b'en-us', max_length=10, verbose_name='language', choices=[(b'af', 'Afrikaans'), (b'ar', '\u0627\u0644\u0639\u0631\u0628\u064a\u0651\u0629'), (b'ast', 'asturianu'), (b'az', 'Az\u0259rbaycanca'), (b'bg', '\u0431\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'), (b'be', '\u0431\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f'), (b'bn', '\u09ac\u09be\u0982\u09b2\u09be'), (b'br', 'brezhoneg'), (b'bs', 'bosanski'), (b'ca', 'catal\xe0'), (b'cs', '\u010desky'), (b'cy', 'Cymraeg'), (b'da', 'dansk'), (b'de', 'Deutsch'), (b'el', '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), (b'en', 'English'), (b'en-au', 'Australian English'), (b'en-gb', 'British English'), (b'eo', 'Esperanto'), (b'es', 'espa\xf1ol'), (b'es-ar', 'espa\xf1ol de Argentina'), (b'es-mx', 'espa\xf1ol de Mexico'), (b'es-ni', 'espa\xf1ol de Nicaragua'), (b'es-ve', 'espa\xf1ol de Venezuela'), (b'et', 'eesti'), (b'eu', 'Basque'), (b'fa', '\u0641\u0627\u0631\u0633\u06cc'), (b'fi', 'suomi'), (b'fr', 'fran\xe7ais'), (b'fy', 'frysk'), (b'ga', 'Gaeilge'), (b'gl', 'galego'), (b'he', '\u05e2\u05d1\u05e8\u05d9\u05ea'), (b'hi', 'Hindi'), (b'hr', 'Hrvatski'), (b'hu', 'Magyar'), (b'ia', 'Interlingua'), (b'id', 'Bahasa Indonesia'), (b'io', 'ido'), (b'is', '\xcdslenska'), (b'it', 'italiano'), (b'ja', '\u65e5\u672c\u8a9e'), (b'ka', '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'), (b'kk', '\u049a\u0430\u0437\u0430\u049b'), (b'km', 'Khmer'), (b'kn', 'Kannada'), (b'ko', '\ud55c\uad6d\uc5b4'), (b'lb', 'L\xebtzebuergesch'), (b'lt', 'Lietuvi\u0161kai'), (b'lv', 'latvie\u0161u'), (b'mk', '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'), (b'ml', 'Malayalam'), (b'mn', 'Mongolian'), (b'mr', '\u092e\u0930\u093e\u0920\u0940'), (b'my', '\u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e\u102c'), (b'nb', 'norsk (bokm\xe5l)'), (b'ne', '\u0928\u0947\u092a\u093e\u0932\u0940'), (b'nl', 'Nederlands'), (b'nn', 'norsk (nynorsk)'), (b'os', '\u0418\u0440\u043e\u043d'), (b'pa', 'Punjabi'), (b'pl', 'polski'), (b'pt', 'Portugu\xeas'), (b'pt-br', 'Portugu\xeas Brasileiro'), (b'ro', 'Rom\xe2n\u0103'), (b'ru', '\u0420\u0443\u0441\u0441\u043a\u0438\u0439'), (b'sk', 'slovensk\xfd'), (b'sl', 'Sloven\u0161\u010dina'), (b'sq', 'shqip'), (b'sr', '\u0441\u0440\u043f\u0441\u043a\u0438'), (b'sr-latn', 'srpski (latinica)'), (b'sv', 'svenska'), (b'sw', 'Kiswahili'), (b'ta', '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'), (b'te', '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41'), (b'th', '\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22'), (b'tr', 'T\xfcrk\xe7e'), (b'tt', '\u0422\u0430\u0442\u0430\u0440\u0447\u0430'), (b'udm', '\u0423\u0434\u043c\u0443\u0440\u0442'), (b'uk', '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430'), (b'ur', '\u0627\u0631\u062f\u0648'), (b'vi', 'Ti\xea\u0301ng Vi\xea\u0323t'), (b'zh-cn', '\u7b80\u4f53\u4e2d\u6587'), (b'zh-hans', '\u7b80\u4f53\u4e2d\u6587'), (b'zh-hant', '\u7e41\u9ad4\u4e2d\u6587'), (b'zh-tw', '\u7e41\u9ad4\u4e2d\u6587')])), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='account')), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='user', related_name='account', on_delete=models.CASCADE)), ], ), migrations.CreateModel( @@ -45,7 +45,7 @@ class Migration(migrations.Migration): ('email', models.EmailField(unique=True, max_length=254)), ('verified', models.BooleanField(default=False, verbose_name='verified')), ('primary', models.BooleanField(default=False, verbose_name='primary')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'email address', @@ -59,7 +59,7 @@ class Migration(migrations.Migration): ('created', models.DateTimeField(default=django.utils.timezone.now)), ('sent', models.DateTimeField(null=True)), ('key', models.CharField(unique=True, max_length=64)), - ('email_address', models.ForeignKey(to='account.EmailAddress')), + ('email_address', models.ForeignKey(to='account.EmailAddress', on_delete=models.CASCADE)), ], options={ 'verbose_name': 'email confirmation', @@ -78,7 +78,7 @@ class Migration(migrations.Migration): ('sent', models.DateTimeField(null=True, verbose_name='sent', blank=True)), ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('use_count', models.PositiveIntegerField(default=0, editable=False, verbose_name='use count')), - ('inviter', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ('inviter', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), ], options={ 'verbose_name': 'signup code', @@ -90,8 +90,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('signup_code', models.ForeignKey(to='account.SignupCode')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('signup_code', models.ForeignKey(to='account.SignupCode', on_delete=models.CASCADE)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], ), ] diff --git a/account/mixins.py b/account/mixins.py index 240926dc..8292f234 100644 --- a/account/mixins.py +++ b/account/mixins.py @@ -2,6 +2,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME +from account.compat import is_authenticated from account.conf import settings from account.utils import handle_redirect_to_login @@ -15,7 +16,7 @@ def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs - if request.user.is_authenticated(): + if is_authenticated(request.user): return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) return self.redirect_to_login() diff --git a/account/models.py b/account/models.py index 9b18c92c..ef3e08bc 100644 --- a/account/models.py +++ b/account/models.py @@ -8,7 +8,6 @@ except ImportError: # python 2 from urllib import urlencode -from django.core.urlresolvers import reverse from django.db import models, transaction from django.db.models import Q from django.db.models.signals import post_save @@ -23,6 +22,7 @@ import pytz from account import signals +from account.compat import reverse, is_authenticated from account.conf import settings from account.fields import TimeZoneField from account.hooks import hookset @@ -33,7 +33,7 @@ @python_2_unicode_compatible class Account(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="account", verbose_name=_("user")) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="account", verbose_name=_("user"), on_delete=models.CASCADE) timezone = TimeZoneField(_("timezone")) language = models.CharField( _("language"), @@ -45,7 +45,7 @@ class Account(models.Model): @classmethod def for_request(cls, request): user = getattr(request, "user", None) - if user and user.is_authenticated(): + if user and is_authenticated(user): try: return Account._default_manager.get(user=user) except Account.DoesNotExist: @@ -140,7 +140,7 @@ class InvalidCode(Exception): code = models.CharField(_("code"), max_length=64, unique=True) max_uses = models.PositiveIntegerField(_("max uses"), default=0) expiry = models.DateTimeField(_("expiry"), null=True, blank=True) - inviter = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) + inviter = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE) email = models.EmailField(max_length=254, blank=True) notes = models.TextField(_("notes"), blank=True) sent = models.DateTimeField(_("sent"), null=True, blank=True) @@ -242,8 +242,8 @@ def send(self, **kwargs): class SignupCodeResult(models.Model): - signup_code = models.ForeignKey(SignupCode) - user = models.ForeignKey(settings.AUTH_USER_MODEL) + signup_code = models.ForeignKey(SignupCode, on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) timestamp = models.DateTimeField(default=timezone.now) def save(self, **kwargs): @@ -254,7 +254,7 @@ def save(self, **kwargs): @python_2_unicode_compatible class EmailAddress(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) email = models.EmailField(max_length=254, unique=settings.ACCOUNT_EMAIL_UNIQUE) verified = models.BooleanField(_("verified"), default=False) primary = models.BooleanField(_("primary"), default=False) @@ -305,7 +305,7 @@ def change(self, new_email, confirm=True): @python_2_unicode_compatible class EmailConfirmation(models.Model): - email_address = models.ForeignKey(EmailAddress) + email_address = models.ForeignKey(EmailAddress, on_delete=models.CASCADE) created = models.DateTimeField(default=timezone.now) sent = models.DateTimeField(null=True) key = models.CharField(max_length=64, unique=True) @@ -400,7 +400,7 @@ class Meta: verbose_name = _("password history") verbose_name_plural = _("password histories") - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="password_history", on_delete=models.CASCADE) password = models.CharField(max_length=255) # encrypted password timestamp = models.DateTimeField(default=timezone.now) # password creation time @@ -409,5 +409,5 @@ class PasswordExpiry(models.Model): """ Holds the password expiration period for a single user. """ - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="password_expiry", verbose_name=_("user")) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="password_expiry", verbose_name=_("user"), on_delete=models.CASCADE) expiry = models.PositiveIntegerField(default=0) diff --git a/account/tests/test_password.py b/account/tests/test_password.py index e05cce3b..c489216b 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -8,13 +8,13 @@ make_password, ) from django.contrib.auth.models import User -from django.core.urlresolvers import reverse from django.test import ( TestCase, modify_settings, override_settings, ) +from ..compat import reverse from ..models import ( PasswordExpiry, PasswordHistory, diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 9444ea38..1d64fadf 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -1,10 +1,10 @@ from django.conf import settings from django.core import mail -from django.core.urlresolvers import reverse from django.test import TestCase, override_settings from django.contrib.auth.models import User +from account.compat import reverse from account.models import SignupCode, EmailConfirmation diff --git a/account/utils.py b/account/utils.py index cb084d5a..6d3078f3 100644 --- a/account/utils.py +++ b/account/utils.py @@ -8,12 +8,12 @@ except ImportError: # python 2 from urlparse import urlparse, urlunparse -from django.core import urlresolvers from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict from django.contrib.auth import get_user_model +from account.compat import reverse, NoReverseMatch from account.conf import settings from .models import PasswordHistory @@ -45,8 +45,8 @@ def default_redirect(request, fallback_url, **kwargs): return next_url else: try: - fallback_url = urlresolvers.reverse(fallback_url) - except urlresolvers.NoReverseMatch: + fallback_url = reverse(fallback_url) + except NoReverseMatch: if callable(fallback_url): raise if "/" not in fallback_url and "." not in fallback_url: @@ -89,8 +89,8 @@ def handle_redirect_to_login(request, **kwargs): if next_url is None: next_url = request.get_full_path() try: - login_url = urlresolvers.reverse(login_url) - except urlresolvers.NoReverseMatch: + login_url = reverse(login_url) + except NoReverseMatch: if callable(login_url): raise if "/" not in login_url and "." not in login_url: diff --git a/account/views.py b/account/views.py index e1513599..9bcd1898 100644 --- a/account/views.py +++ b/account/views.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django.core.urlresolvers import reverse from django.http import Http404, HttpResponseForbidden from django.shortcuts import redirect, get_object_or_404 from django.utils.http import base36_to_int, int_to_base36 @@ -15,6 +14,7 @@ from django.contrib.sites.shortcuts import get_current_site from account import signals +from account.compat import reverse, is_authenticated from account.conf import settings from account.forms import SignupForm, LoginUsernameForm from account.forms import ChangePasswordForm, PasswordResetForm, PasswordResetTokenForm @@ -156,14 +156,14 @@ def setup_signup_code(self): self.signup_code_present = False def get(self, *args, **kwargs): - if self.request.user.is_authenticated(): + if is_authenticated(self.request.user): return redirect(default_redirect(self.request, settings.ACCOUNT_LOGIN_REDIRECT_URL)) if not self.is_open(): return self.closed() return super(SignupView, self).get(*args, **kwargs) def post(self, *args, **kwargs): - if self.request.user.is_authenticated(): + if is_authenticated(self.request.user): raise Http404() if not self.is_open(): return self.closed() @@ -343,7 +343,7 @@ class LoginView(FormView): redirect_field_name = "next" def get(self, *args, **kwargs): - if self.request.user.is_authenticated(): + if is_authenticated(self.request.user): return redirect(self.get_success_url()) return super(LoginView, self).get(*args, **kwargs) @@ -404,13 +404,13 @@ class LogoutView(TemplateResponseMixin, View): redirect_field_name = "next" def get(self, *args, **kwargs): - if not self.request.user.is_authenticated(): + if not is_authenticated(self.request.user): return redirect(self.get_redirect_url()) ctx = self.get_context_data() return self.render_to_response(ctx) def post(self, *args, **kwargs): - if self.request.user.is_authenticated(): + if is_authenticated(self.request.user): auth.logout(self.request) return redirect(self.get_redirect_url()) @@ -495,7 +495,7 @@ def get_context_data(self, **kwargs): return ctx def get_redirect_url(self): - if self.user.is_authenticated(): + if is_authenticated(self.user): if not settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL: return settings.ACCOUNT_LOGIN_REDIRECT_URL return settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL @@ -528,12 +528,12 @@ class ChangePasswordView(PasswordMixin, FormView): fallback_url_setting = "ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL" def get(self, *args, **kwargs): - if not self.request.user.is_authenticated(): + if not is_authenticated(self.request.user): return redirect("account_password_reset") return super(ChangePasswordView, self).get(*args, **kwargs) def post(self, *args, **kwargs): - if not self.request.user.is_authenticated(): + if not is_authenticated(self.request.user): return HttpResponseForbidden() return super(ChangePasswordView, self).post(*args, **kwargs) diff --git a/runtests.py b/runtests.py index 471171eb..ccd776f2 100644 --- a/runtests.py +++ b/runtests.py @@ -52,12 +52,19 @@ ] ) -DEFAULT_SETTINGS["MIDDLEWARE" if django.VERSION >= (1, 10) else "MIDDLEWARE_CLASSES"] = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", -] +if django.VERSION >= (1, 10): + DEFAULT_SETTINGS["MIDDLEWARE"] = [ + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ] +else: + DEFAULT_SETTINGS["MIDDLEWARE_CLASSES"] = [ + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + ] def runtests(*test_args): diff --git a/setup.py b/setup.py index 1fcebec4..9ae8aa70 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ url="http://github.com/pinax/django-user-accounts", packages=find_packages(), install_requires=[ + "Django>=1.8", "django-appconf>=1.0.1", "pytz>=2015.6" ], From 0ed43287092a327a376d5b5693a41cb366c07e12 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 18 Apr 2017 12:40:18 -0500 Subject: [PATCH 072/239] 2.0.1 --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 8c0d5d5b..159d48b8 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" From 4b1fe5076e80880c6c09ca3829e7b6f746eb399b Mon Sep 17 00:00:00 2001 From: Alexey Milyutin Date: Wed, 10 May 2017 08:51:39 +0300 Subject: [PATCH 073/239] #249 fix. Add new setting ACCOUNT_PASSWORD_RESET_TOKEN_URL --- account/conf.py | 1 + account/views.py | 2 +- docs/settings.rst | 9 +++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/account/conf.py b/account/conf.py index 4e620af2..4208614a 100644 --- a/account/conf.py +++ b/account/conf.py @@ -38,6 +38,7 @@ class AccountAppConf(AppConf): LOGOUT_REDIRECT_URL = "/" PASSWORD_CHANGE_REDIRECT_URL = "account_password" PASSWORD_RESET_REDIRECT_URL = "account_login" + PASSWORD_RESET_TOKEN_URL = "account_password_reset_token" PASSWORD_EXPIRY = 0 PASSWORD_USE_HISTORY = False PASSWORD_STRIP = True diff --git a/account/views.py b/account/views.py index 9bcd1898..78bc149e 100644 --- a/account/views.py +++ b/account/views.py @@ -598,7 +598,7 @@ def send_email(self, email): password_reset_url = "{0}://{1}{2}".format( protocol, current_site.domain, - reverse("account_password_reset_token", kwargs=dict(uidb36=uid, token=token)) + reverse(settings.ACCOUNT_PASSWORD_RESET_TOKEN_URL, kwargs=dict(uidb36=uid, token=token)) ) ctx = { "user": user, diff --git a/docs/settings.rst b/docs/settings.rst index 6cb76db3..ed3fe5d6 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -40,13 +40,18 @@ Default: ``"account_password"`` Default: ``"account_login"`` +``ACCOUNT_PASSWORD_RESET_TOKEN_URL`` +==================================== + +Default: ``"account_password_reset_token"`` + ``ACCOUNT_PASSWORD_EXPIRY`` -======================================= +=========================== Default: ``0`` ``ACCOUNT_PASSWORD_USE_HISTORY`` -======================================= +================================ Default: ``False`` From b75cec52a8812d885c60145d715896dc85b2c4b2 Mon Sep 17 00:00:00 2001 From: Cezary Statkiewicz Date: Fri, 12 May 2017 14:02:14 +0200 Subject: [PATCH 074/239] allow admin to manually enable user after signing up #255 --- account/conf.py | 4 ++ .../account/admin_approval_sent.html | 0 account/tests/test_views.py | 19 ++++++ account/views.py | 60 +++++++++++++------ docs/settings.rst | 10 ++++ 5 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 account/tests/templates/account/admin_approval_sent.html diff --git a/account/conf.py b/account/conf.py index 4e620af2..3287dfd6 100644 --- a/account/conf.py +++ b/account/conf.py @@ -40,6 +40,7 @@ class AccountAppConf(AppConf): PASSWORD_RESET_REDIRECT_URL = "account_login" PASSWORD_EXPIRY = 0 PASSWORD_USE_HISTORY = False + ACCOUNT_APPROVAL_REQUIRED = False PASSWORD_STRIP = True REMEMBER_ME_EXPIRY = 60 * 60 * 24 * 365 * 10 USER_DISPLAY = lambda user: user.username # flake8: noqa @@ -61,3 +62,6 @@ class AccountAppConf(AppConf): def configure_hookset(self, value): return load_path_attr(value)() + + + diff --git a/account/tests/templates/account/admin_approval_sent.html b/account/tests/templates/account/admin_approval_sent.html new file mode 100644 index 00000000..e69de29b diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 1d64fadf..5ee390a9 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -49,6 +49,8 @@ def test_valid_code(self): } response = self.client.post(reverse("account_signup"), data) self.assertEqual(response.status_code, 302) + u = User.objects.get(username=data['username']) + self.assertTrue(u.is_active) def test_invalid_code(self): with self.settings(ACCOUNT_OPEN_SIGNUP=False): @@ -124,6 +126,23 @@ def test_session_next_url(self): self.assertRedirects(response, next_url, fetch_redirect_response=False) + def test_register_with_moderation(self): + signup_code = SignupCode.create() + signup_code.save() + with self.settings(ACCOUNT_OPEN_SIGNUP=True, ACCOUNT_APPROVAL_REQUIRED=True): + data = { + "username": "foo", + "password": "bar", + "password_confirm": "bar", + "email": "foobar@example.com", + "code": signup_code.code, + } + response = self.client.post(reverse("account_signup"), data) + self.assertEqual(response.status_code, 200) + self.assertFalse(self.client.session.get('_auth_user_id')) + u = User.objects.get(username=data['username']) + self.assertFalse(u.is_active) + class LoginViewTestCase(TestCase): def signup(self): diff --git a/account/views.py b/account/views.py index 9bcd1898..49b28ce7 100644 --- a/account/views.py +++ b/account/views.py @@ -114,6 +114,8 @@ class SignupView(PasswordMixin, FormView): template_name_email_confirmation_sent_ajax = "account/ajax/email_confirmation_sent.html" template_name_signup_closed = "account/signup_closed.html" template_name_signup_closed_ajax = "account/ajax/signup_closed.html" + template_name_admin_approval_sent = "account/admin_approval_sent.html" + template_name_admin_approval_sent_ajax = "account/ajax/admin_approval_sent.html" form_class = SignupForm form_kwargs = {} form_password_field = "password" @@ -211,29 +213,34 @@ def form_valid(self, form): self.create_account(form) self.create_password_history(form, self.created_user) self.after_signup(form) + if settings.ACCOUNT_APPROVAL_REQUIRED: + # Notify site admins about the user wanting activation + self.created_user.is_active = False + self.created_user.save() + return self.account_approval_required_response() if settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL and not email_address.verified: self.send_email_confirmation(email_address) if settings.ACCOUNT_EMAIL_CONFIRMATION_REQUIRED and not email_address.verified: return self.email_confirmation_required_response() - else: - show_message = [ - settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL, - self.messages.get("email_confirmation_sent"), - not email_address.verified - ] - if all(show_message): - messages.add_message( - self.request, - self.messages["email_confirmation_sent"]["level"], - self.messages["email_confirmation_sent"]["text"].format(**{ - "email": form.cleaned_data["email"] - }) - ) - # attach form to self to maintain compatibility with login_user - # API. this should only be relied on by d-u-a and it is not a stable - # API for site developers. - self.form = form - self.login_user() + show_message = [ + settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL, + self.messages.get("email_confirmation_sent"), + not email_address.verified + ] + if all(show_message): + messages.add_message( + self.request, + self.messages["email_confirmation_sent"]["level"], + self.messages["email_confirmation_sent"]["text"].format(**{ + "email": form.cleaned_data["email"] + }) + ) + # attach form to self to maintain compatibility with login_user + # API. this should only be relied on by d-u-a and it is not a stable + # API for site developers. + self.form = form + + self.login_user() return redirect(self.get_success_url()) def create_user(self, form, commit=True, model=None, **kwargs): @@ -333,6 +340,21 @@ def closed(self): } return self.response_class(**response_kwargs) + def account_approval_required_response(self): + if self.request.is_ajax(): + template_name = self.template_name_admin_approval_ajax + else: + template_name = self.template_name_admin_approval_sent + + response_kwargs = { + "request": self.request, + "template": template_name, + "context": { + "email": self.created_user.email, + "success_url": self.get_success_url(), + } + } + return self.response_class(**response_kwargs) class LoginView(FormView): diff --git a/docs/settings.rst b/docs/settings.rst index 6cb76db3..ffc49dc9 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -148,3 +148,13 @@ Default: ``list(zip(pytz.all_timezones, pytz.all_timezones))`` ===================== See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/language_list.py + + +``ACCOUNT_APPROVAL_REQUIRED`` +================================== + +Default: ``False`` + +This setting will make new registrations inactive, until staff will set ``is_active`` + flag in admin panel. Additional integration (like sending notifications to staff) +is possible with ``account.signals.user_signed_up`` signal. From 9333e20162ffbbf5920a36af7c9a49ec56da87d3 Mon Sep 17 00:00:00 2001 From: Cezary Statkiewicz Date: Fri, 12 May 2017 14:05:05 +0200 Subject: [PATCH 075/239] flake8 fixes --- account/conf.py | 3 --- account/tests/test_views.py | 2 +- account/views.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/account/conf.py b/account/conf.py index 3287dfd6..eb6adc77 100644 --- a/account/conf.py +++ b/account/conf.py @@ -62,6 +62,3 @@ class AccountAppConf(AppConf): def configure_hookset(self, value): return load_path_attr(value)() - - - diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 5ee390a9..00b812be 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -125,7 +125,6 @@ def test_session_next_url(self): response = self.client.post(reverse("account_signup"), data) self.assertRedirects(response, next_url, fetch_redirect_response=False) - def test_register_with_moderation(self): signup_code = SignupCode.create() signup_code.save() @@ -143,6 +142,7 @@ def test_register_with_moderation(self): u = User.objects.get(username=data['username']) self.assertFalse(u.is_active) + class LoginViewTestCase(TestCase): def signup(self): diff --git a/account/views.py b/account/views.py index 49b28ce7..b623d55d 100644 --- a/account/views.py +++ b/account/views.py @@ -239,7 +239,6 @@ def form_valid(self, form): # API. this should only be relied on by d-u-a and it is not a stable # API for site developers. self.form = form - self.login_user() return redirect(self.get_success_url()) @@ -356,6 +355,7 @@ def account_approval_required_response(self): } return self.response_class(**response_kwargs) + class LoginView(FormView): template_name = "account/login.html" From 45aef3db59fed1626e551cd348a79f9554677edd Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 11:31:14 -0600 Subject: [PATCH 076/239] Fixed potentional security issue with leaked password tokens Django 1.11+ prevents password tokens from being leaked through the HTTP Referer header if a template calls out to third-party resources (i.e., JS or CSS) by setting token in session and redirecting. This change brings that change to PasswordResetTokenView. Fixes #258 --- CHANGELOG.md | 8 +++ .../account/email/password_reset.txt | 1 + .../account/email/password_reset_subject.txt | 1 + .../account/password_reset_sent.html | 1 + .../account/password_reset_token.html | 1 + account/tests/test_views.py | 67 +++++++++++++++++++ account/views.py | 33 ++++++--- 7 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 account/tests/templates/account/email/password_reset.txt create mode 100644 account/tests/templates/account/email/password_reset_subject.txt create mode 100644 account/tests/templates/account/password_reset_sent.html create mode 100644 account/tests/templates/account/password_reset_token.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a82c1c5..f0626f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 2.0.2 + + * fixed potentional security issue with leaking password reset tokens through HTTP Referer header + +## 2.0.1 + +@@@ todo + ## 2.0.0 * BI: moved account deletion callbacks to hooksets diff --git a/account/tests/templates/account/email/password_reset.txt b/account/tests/templates/account/email/password_reset.txt new file mode 100644 index 00000000..ad84fec3 --- /dev/null +++ b/account/tests/templates/account/email/password_reset.txt @@ -0,0 +1 @@ +{{ password_reset_url }} diff --git a/account/tests/templates/account/email/password_reset_subject.txt b/account/tests/templates/account/email/password_reset_subject.txt new file mode 100644 index 00000000..e965047a --- /dev/null +++ b/account/tests/templates/account/email/password_reset_subject.txt @@ -0,0 +1 @@ +Hello diff --git a/account/tests/templates/account/password_reset_sent.html b/account/tests/templates/account/password_reset_sent.html new file mode 100644 index 00000000..9ed72bb4 --- /dev/null +++ b/account/tests/templates/account/password_reset_sent.html @@ -0,0 +1 @@ +# empty for now diff --git a/account/tests/templates/account/password_reset_token.html b/account/tests/templates/account/password_reset_token.html new file mode 100644 index 00000000..9ed72bb4 --- /dev/null +++ b/account/tests/templates/account/password_reset_token.html @@ -0,0 +1 @@ +# empty for now diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 1d64fadf..1247691d 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -1,6 +1,8 @@ from django.conf import settings from django.core import mail from django.test import TestCase, override_settings +from django.utils.http import int_to_base36 +from django.utils.six.moves.urllib.parse import urlparse from django.contrib.auth.models import User @@ -348,3 +350,68 @@ def test_post_authenticated_success_no_mail(self): fetch_redirect_response=False ) self.assertEqual(len(mail.outbox), 0) + + +class PasswordResetTokenViewTestCase(TestCase): + + def signup(self): + data = { + "username": "foo", + "password": "bar", + "password_confirm": "bar", + "email": "foobar@example.com", + "code": "abc123", + } + self.client.post(reverse("account_signup"), data) + mail.outbox = [] + return User.objects.get(username="foo") + + def request_password_reset(self): + user = self.signup() + data = { + "email": user.email, + } + self.client.post(reverse("account_password_reset"), data) + parsed = urlparse(mail.outbox[0].body.strip()) + return user, parsed.path + + def test_get_bad_user(self): + url = reverse( + "account_password_reset_token", + kwargs={ + "uidb36": int_to_base36(100), + "token": "notoken", + } + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_get_reset(self): + user, url = self.request_password_reset() + response = self.client.get(url) + self.assertRedirects( + response, + reverse( + "account_password_reset_token", + kwargs={ + "uidb36": int_to_base36(user.id), + "token": "set-password", + } + ), + fetch_redirect_response=False + ) + + def test_post_reset(self): + user, url = self.request_password_reset() + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + data = { + "password": "new-password", + "password_confirm": "new-password", + } + response = self.client.post(response["Location"], data) + self.assertRedirects( + response, + reverse(settings.ACCOUNT_PASSWORD_RESET_REDIRECT_URL), + fetch_redirect_response=False + ) diff --git a/account/views.py b/account/views.py index 9bcd1898..89600327 100644 --- a/account/views.py +++ b/account/views.py @@ -611,6 +611,10 @@ def make_token(self, user): return self.token_generator.make_token(user) +INTERNAL_RESET_URL_TOKEN = "set-password" +INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token" + + class PasswordResetTokenView(PasswordMixin, FormView): template_name = "account/password_reset_token.html" @@ -620,22 +624,31 @@ class PasswordResetTokenView(PasswordMixin, FormView): form_password_field = "password" fallback_url_setting = "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" + def dispatch(self, *args, **kwargs): + user = self.get_user() + if user is not None: + token = kwargs["token"] + if token == INTERNAL_RESET_URL_TOKEN: + session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN) + if self.check_token(user, session_token): + return super(PasswordResetTokenView, self).dispatch(*args, **kwargs) + else: + if self.check_token(user, token): + # Store the token in the session and redirect to the + # password reset form at a URL without the token. That + # avoids the possibility of leaking the token in the + # HTTP Referer header. + self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token + redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN) + return redirect(redirect_url) + return self.token_fail() + def get(self, request, **kwargs): form_class = self.get_form_class() form = self.get_form(form_class) ctx = self.get_context_data(form=form) - if not self.check_token(self.get_user(), self.kwargs["token"]): - return self.token_fail() return self.render_to_response(ctx) - def get_context_data(self, **kwargs): - ctx = super(PasswordResetTokenView, self).get_context_data(**kwargs) - ctx.update({ - "uidb36": self.kwargs["uidb36"], - "token": self.kwargs["token"], - }) - return ctx - def form_valid(self, form): self.change_password(form) self.create_password_history(form, self.request.user) From 2501b1bb32d98cb561fa131b5f2bf61de76f852e Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 12:25:39 -0600 Subject: [PATCH 077/239] Added never_cache, csrf_protect and sensitive_post_parameters to appropriate views --- CHANGELOG.md | 1 + account/views.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0626f17..ed1fd930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ version with these. Your code will need to be updated to continue working. ## 2.0.2 * fixed potentional security issue with leaking password reset tokens through HTTP Referer header + * added `never_cache`, `csrf_protect` and `sensitive_post_parameters` to appropriate views ## 2.0.1 diff --git a/account/views.py b/account/views.py index 89600327..9e0cca7e 100644 --- a/account/views.py +++ b/account/views.py @@ -2,8 +2,12 @@ from django.http import Http404, HttpResponseForbidden from django.shortcuts import redirect, get_object_or_404 +from django.utils.decorators import method_decorator from django.utils.http import base36_to_int, int_to_base36 from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateResponseMixin, View from django.views.generic.edit import FormView @@ -136,6 +140,9 @@ def __init__(self, *args, **kwargs): kwargs["signup_code"] = None super(SignupView, self).__init__(*args, **kwargs) + @method_decorator(sensitive_post_parameters()) + @method_decorator(csrf_protect) + @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): self.request = request self.args = args @@ -342,6 +349,12 @@ class LoginView(FormView): form_kwargs = {} redirect_field_name = "next" + @method_decorator(sensitive_post_parameters()) + @method_decorator(csrf_protect) + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + return super(LoginView, self).dispatch(*args, **kwargs) + def get(self, *args, **kwargs): if is_authenticated(self.request.user): return redirect(self.get_success_url()) @@ -403,6 +416,10 @@ class LogoutView(TemplateResponseMixin, View): template_name = "account/logout.html" redirect_field_name = "next" + @method_decorator(never_cache) + def dispatch(self, *args, **kwargs): + return super(LogoutView, self).dispatch(*args, **kwargs) + def get(self, *args, **kwargs): if not is_authenticated(self.request.user): return redirect(self.get_redirect_url()) @@ -572,6 +589,10 @@ class PasswordResetView(FormView): form_class = PasswordResetForm token_generator = default_token_generator + @method_decorator(csrf_protect) + def dispatch(self, *args, **kwargs): + return super(PasswordResetView, self).dispatch(*args, **kwargs) + def get_context_data(self, **kwargs): context = super(PasswordResetView, self).get_context_data(**kwargs) if self.request.method == "POST" and "resend" in self.request.POST: @@ -624,6 +645,8 @@ class PasswordResetTokenView(PasswordMixin, FormView): form_password_field = "password" fallback_url_setting = "ACCOUNT_PASSWORD_RESET_REDIRECT_URL" + @method_decorator(sensitive_post_parameters()) + @method_decorator(never_cache) def dispatch(self, *args, **kwargs): user = self.get_user() if user is not None: From af3536c8c3f3d8ec0974aaf093c58ac4fff1c2a5 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 12:33:52 -0600 Subject: [PATCH 078/239] v2.0.2 --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 159d48b8..0309ae29 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "2.0.1" +__version__ = "2.0.2" From 51900daa52f9895f9253bd83e43a47a65777498f Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 13:01:46 -0600 Subject: [PATCH 079/239] Fixed breaking change in 2.0.2 where context did not have uidb36 and token --- CHANGELOG.md | 4 ++++ account/views.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1fd930..ed53cbdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 2.0.3 + + * fixed breaking change in 2.0.2 where context did not have uidb36 and token + ## 2.0.2 * fixed potentional security issue with leaking password reset tokens through HTTP Referer header diff --git a/account/views.py b/account/views.py index 9e0cca7e..e58eda25 100644 --- a/account/views.py +++ b/account/views.py @@ -672,6 +672,14 @@ def get(self, request, **kwargs): ctx = self.get_context_data(form=form) return self.render_to_response(ctx) + def get_context_data(self, **kwargs): + ctx = super(PasswordResetTokenView, self).get_context_data(**kwargs) + ctx.update({ + "uidb36": self.kwargs["uidb36"], + "token": self.kwargs["token"], + }) + return ctx + def form_valid(self, form): self.change_password(form) self.create_password_history(form, self.request.user) From fb447c725589f5bc5825e32786b43a5eedbe816e Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 13:03:33 -0600 Subject: [PATCH 080/239] CHANGELOG update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed53cbdb..e6cf1114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ version with these. Your code will need to be updated to continue working. ## 2.0.3 * fixed breaking change in 2.0.2 where context did not have uidb36 and token + * improved documentation ## 2.0.2 From e90de89e8028f2d4db4b2a9572a964cc8c50a2ba Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 9 Jun 2017 13:12:34 -0600 Subject: [PATCH 081/239] v2.0.3 --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 0309ae29..5fa9130a 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "2.0.2" +__version__ = "2.0.3" From 8bf5412b811a08f7fdfcbb4e6930a6f4c227a031 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Wed, 23 Aug 2017 09:35:35 -0600 Subject: [PATCH 082/239] Fixed email confirmation to correctly check if expired --- account/views.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/account/views.py b/account/views.py index e58eda25..bdf8e09f 100644 --- a/account/views.py +++ b/account/views.py @@ -457,6 +457,10 @@ class ConfirmEmailView(TemplateResponseMixin, View): "email_confirmed": { "level": messages.SUCCESS, "text": _("You have confirmed {email}.") + }, + "email_confirmation_expired": { + "level": messages.ERROR, + "text": _("Email confirmation for {email} has expired.") } } @@ -473,21 +477,30 @@ def get(self, *args, **kwargs): def post(self, *args, **kwargs): self.object = confirmation = self.get_object() - confirmation.confirm() - self.after_confirmation(confirmation) - if settings.ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN: - self.user = self.login_user(confirmation.email_address.user) + self.user = self.request.user + confirmed = confirmation.confirm() is not None + if confirmed: + self.after_confirmation(confirmation) + if settings.ACCOUNT_EMAIL_CONFIRMATION_AUTO_LOGIN: + self.user = self.login_user(confirmation.email_address.user) + redirect_url = self.get_redirect_url() + if not redirect_url: + ctx = self.get_context_data() + return self.render_to_response(ctx) + if self.messages.get("email_confirmed"): + messages.add_message( + self.request, + self.messages["email_confirmed"]["level"], + self.messages["email_confirmed"]["text"].format(**{ + "email": confirmation.email_address.email + }) + ) else: - self.user = self.request.user - redirect_url = self.get_redirect_url() - if not redirect_url: - ctx = self.get_context_data() - return self.render_to_response(ctx) - if self.messages.get("email_confirmed"): + redirect_url = self.get_redirect_url() messages.add_message( self.request, - self.messages["email_confirmed"]["level"], - self.messages["email_confirmed"]["text"].format(**{ + self.messages["email_confirmation_expired"]["level"], + self.messages["email_confirmation_expired"]["text"].format(**{ "email": confirmation.email_address.email }) ) From 3bdd32b831dba9c9da0baaae61ffbc052722ec6a Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Wed, 23 Aug 2017 09:42:46 -0600 Subject: [PATCH 083/239] Updated README to reflect Python / Django support I suspect we'll need to revisit this and remove some older versions from support based on Django. Fixes #261 --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ad2f4333..41b5e7ab 100644 --- a/README.rst +++ b/README.rst @@ -58,8 +58,8 @@ Features Requirements -------------- -* Django 1.8, 1.9, or 1.10 -* Python 2.7, 3.3, 3.4 or 3.5 +* Django 1.8, 1.9, 1.10 and 1.11 +* Python 2.7, 3.3, 3.4, 3.5 and 3.6 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) From 82af57968cf2281ba5a6c5f10e3be311a3e271c2 Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Wed, 23 Aug 2017 13:31:52 -0400 Subject: [PATCH 084/239] Fix bug in PasswordResetConfirm logic if token is missing from session --- .../account/password_reset_token_fail.html | 1 + account/tests/test_views.py | 17 ++++++++++++++++- account/views.py | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 account/tests/templates/account/password_reset_token_fail.html diff --git a/account/tests/templates/account/password_reset_token_fail.html b/account/tests/templates/account/password_reset_token_fail.html new file mode 100644 index 00000000..9ed72bb4 --- /dev/null +++ b/account/tests/templates/account/password_reset_token_fail.html @@ -0,0 +1 @@ +# empty for now diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 1247691d..9af5f01d 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -8,6 +8,7 @@ from account.compat import reverse from account.models import SignupCode, EmailConfirmation +from account.views import INTERNAL_RESET_URL_TOKEN, PasswordResetTokenView class SignupViewTestCase(TestCase): @@ -386,6 +387,20 @@ def test_get_bad_user(self): response = self.client.get(url) self.assertEqual(response.status_code, 404) + def test_get_abuse_reset_token(self): + user = self.signup() + url = reverse( + "account_password_reset_token", + kwargs={ + "uidb36": int_to_base36(user.id), + "token": INTERNAL_RESET_URL_TOKEN, + } + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, + PasswordResetTokenView.template_name_fail) + def test_get_reset(self): user, url = self.request_password_reset() response = self.client.get(url) @@ -395,7 +410,7 @@ def test_get_reset(self): "account_password_reset_token", kwargs={ "uidb36": int_to_base36(user.id), - "token": "set-password", + "token": INTERNAL_RESET_URL_TOKEN, } ), fetch_redirect_response=False diff --git a/account/views.py b/account/views.py index bdf8e09f..84cb98bf 100644 --- a/account/views.py +++ b/account/views.py @@ -665,7 +665,7 @@ def dispatch(self, *args, **kwargs): if user is not None: token = kwargs["token"] if token == INTERNAL_RESET_URL_TOKEN: - session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN) + session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN, '') if self.check_token(user, session_token): return super(PasswordResetTokenView, self).dispatch(*args, **kwargs) else: From 9162007d402ab8eca56de586c99bba94c1c58d0a Mon Sep 17 00:00:00 2001 From: namkan Date: Sun, 27 Aug 2017 21:11:18 +0530 Subject: [PATCH 085/239] Now DeleteView contains LoginRequiredMixin Closes #263 --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index bdf8e09f..88ad9a89 100644 --- a/account/views.py +++ b/account/views.py @@ -804,7 +804,7 @@ def get_success_url(self, fallback_url=None, **kwargs): return default_redirect(self.request, fallback_url, **kwargs) -class DeleteView(LogoutView): +class DeleteView(LoginRequiredMixin, LogoutView): template_name = "account/delete.html" messages = { From 8ef7dada32ee9e09b80a5acc80f5d5f0aae064ca Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Tue, 3 Oct 2017 17:51:38 -0500 Subject: [PATCH 086/239] add DEFAUT_LANGUAGE constant --- account/languages.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/account/languages.py b/account/languages.py index cc9a5cce..059fed25 100644 --- a/account/languages.py +++ b/account/languages.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.conf import settings +from django.utils.translation import get_language_info + +""" # List of language code and languages local names. # # This list is output of code: -# [ -# (code, get_language_info(code).get("name_local")) -# for code, lang in settings.LANGUAGES -# ] -# + +[ + (code, get_language_info(code).get("name_local")) + for code, lang in settings.LANGUAGES +] +""" LANGUAGES = [ ("af", "Afrikaans"), @@ -98,3 +103,5 @@ ("zh-hant", "繁體中文"), ("zh-tw", "繁體中文") ] + +DEFAULT_LANGUAGE = get_language_info(settings.LANGUAGE_CODE)["code"] From e1622f61f741d1eb9157d2504a3debf057a091a0 Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Tue, 3 Oct 2017 17:54:36 -0500 Subject: [PATCH 087/239] prefer DEFAULT_LANGUAGE over settings.LANGUAGE_CODE --- account/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/account/models.py b/account/models.py index ef3e08bc..2ac019ca 100644 --- a/account/models.py +++ b/account/models.py @@ -28,6 +28,7 @@ from account.hooks import hookset from account.managers import EmailAddressManager, EmailConfirmationManager from account.signals import signup_code_sent, signup_code_used +from account.languages import DEFAULT_LANGUAGE @python_2_unicode_compatible @@ -39,7 +40,7 @@ class Account(models.Model): _("language"), max_length=10, choices=settings.ACCOUNT_LANGUAGES, - default=settings.LANGUAGE_CODE + default=DEFAULT_LANGUAGE, ) @classmethod @@ -59,7 +60,7 @@ def create(cls, request=None, **kwargs): account = cls(**kwargs) if "language" not in kwargs: if request is None: - account.language = settings.LANGUAGE_CODE + account.language = DEFAULT_LANGUAGE else: account.language = translation.get_language_from_request(request, check_path=True) account.save() @@ -120,7 +121,7 @@ def __init__(self, request=None): self.user = AnonymousUser() self.timezone = settings.TIME_ZONE if request is None: - self.language = settings.LANGUAGE_CODE + self.language = DEFAULT_LANGUAGE else: self.language = translation.get_language_from_request(request, check_path=True) From 76b054a0adcab45f9ab723f620f1b345e29ea397 Mon Sep 17 00:00:00 2001 From: Jacob Wegner Date: Tue, 3 Oct 2017 18:11:06 -0500 Subject: [PATCH 088/239] add migration for DEFAULT_LANGUAGE --- .../0005_update_default_language.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 account/migrations/0005_update_default_language.py diff --git a/account/migrations/0005_update_default_language.py b/account/migrations/0005_update_default_language.py new file mode 100644 index 00000000..8e86cc3f --- /dev/null +++ b/account/migrations/0005_update_default_language.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-10-03 18:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0004_auto_20170416_1821'), + ] + + operations = [ + migrations.AlterField( + model_name='account', + name='language', + field=models.CharField(choices=[('af', 'Afrikaans'), ('ar', '\u0627\u0644\u0639\u0631\u0628\u064a\u0651\u0629'), ('ast', 'asturian'), ('az', 'Az\u0259rbaycanca'), ('bg', '\u0431\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438'), ('be', '\u0431\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f'), ('bn', '\u09ac\u09be\u0982\u09b2\u09be'), ('br', 'brezhoneg'), ('bs', 'bosanski'), ('ca', 'catal\xe0'), ('cs', '\u010desky'), ('cy', 'Cymraeg'), ('da', 'dansk'), ('de', 'Deutsch'), ('el', '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'espa\xf1ol'), ('es-ar', 'espa\xf1ol de Argentina'), ('es-mx', 'espa\xf1ol de Mexico'), ('es-ni', 'espa\xf1ol de Nicaragua'), ('es-ve', 'espa\xf1ol de Venezuela'), ('et', 'eesti'), ('eu', 'Basque'), ('fa', '\u0641\u0627\u0631\u0633\u06cc'), ('fi', 'suomi'), ('fr', 'fran\xe7ais'), ('fy', 'frysk'), ('ga', 'Gaeilge'), ('gl', 'galego'), ('he', '\u05e2\u05d1\u05e8\u05d9\u05ea'), ('hi', 'Hindi'), ('hr', 'Hrvatski'), ('hu', 'Magyar'), ('ia', 'Interlingua'), ('id', 'Bahasa Indonesia'), ('io', 'ido'), ('is', '\xcdslenska'), ('it', 'italiano'), ('ja', '\u65e5\u672c\u8a9e'), ('ka', '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8'), ('kk', '\u049a\u0430\u0437\u0430\u049b'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', '\ud55c\uad6d\uc5b4'), ('lb', 'L\xebtzebuergesch'), ('lt', 'Lietuvi\u0161kai'), ('lv', 'latvie\u0161'), ('mk', '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', '\u092e\u0930\u093e\u0920\u0940'), ('my', '\u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e\u102c'), ('nb', 'norsk (bokm\xe5l)'), ('ne', '\u0928\u0947\u092a\u093e\u0932\u0940'), ('nl', 'Nederlands'), ('nn', 'norsk (nynorsk)'), ('os', '\u0418\u0440\u043e\u043d'), ('pa', 'Punjabi'), ('pl', 'polski'), ('pt', 'Portugu\xeas'), ('pt-br', 'Portugu\xeas Brasileiro'), ('ro', 'Rom\xe2n\u0103'), ('ru', '\u0420\u0443\u0441\u0441\u043a\u0438\u0439'), ('sk', 'slovensk\xfd'), ('sl', 'Sloven\u0161\u010dina'), ('sq', 'shqip'), ('sr', '\u0441\u0440\u043f\u0441\u043a\u0438'), ('sr-latn', 'srpski (latinica)'), ('sv', 'svenska'), ('sw', 'Kiswahili'), ('ta', '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd'), ('te', '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41'), ('th', '\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22'), ('tr', 'T\xfcrk\xe7e'), ('tt', '\u0422\u0430\u0442\u0430\u0440\u0447\u0430'), ('udm', '\u0423\u0434\u043c\u0443\u0440\u0442'), ('uk', '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430'), ('ur', '\u0627\u0631\u062f\u0648'), ('vi', 'Ti\xea\u0301ng Vi\xea\u0323t'), ('zh-cn', '\u7b80\u4f53\u4e2d\u6587'), ('zh-hans', '\u7b80\u4f53\u4e2d\u6587'), ('zh-hant', '\u7e41\u9ad4\u4e2d\u6587'), ('zh-tw', '\u7e41\u9ad4\u4e2d\u6587')], default='en', max_length=10, verbose_name='language'), + ), + ] From d2198b307afaa9d8db5b910a87c37d87b5ad8d8c Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:33:50 -0500 Subject: [PATCH 089/239] Add pinax patch for DUA and fix badges Removed the downloads badge from PyPI because it no longer works. Added badges for Github stats --- README.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 41b5e7ab..f8e1fe93 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +.. image:: http://pinaxproject.com/pinax-design/patches/django-user-accounts.svg + ==================== Django User Accounts ==================== @@ -11,15 +13,16 @@ Django User Accounts .. image:: https://img.shields.io/coveralls/pinax/django-user-accounts.svg :target: https://coveralls.io/r/pinax/django-user-accounts -.. image:: https://img.shields.io/pypi/dm/django-user-accounts.svg - :target: https://pypi.python.org/pypi/django-user-accounts/ - .. image:: https://img.shields.io/pypi/v/django-user-accounts.svg :target: https://pypi.python.org/pypi/django-user-accounts/ .. image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://pypi.python.org/pypi/django-user-accounts/ +.. image:: https://img.shields.io/github/contributors/pinax/pinax-stripe.svg +.. image:: https://img.shields.io/github/issues-pr/pinax/pinax-stripe.svg +.. image:: https://img.shields.io/github/issues-pr-closed/pinax/pinax-stripe.svg + Pinax ------- From aceaadf7a79b25cfe7b7278ddc0e7dec5d7021e0 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:34:02 -0500 Subject: [PATCH 090/239] Ignore build directories --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 46f1e5d1..058eb4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pyc .coverage *.egg-info +build/ +dist/ From f8deb53d6474b91706381f94d74218be27ad2f50 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:34:28 -0500 Subject: [PATCH 091/239] Add checking for single-quotes --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7c77d849..4d5ffa71 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,8 @@ ignore = E265,E501 max-line-length = 100 max-complexity = 10 -exclude = migrations/*,docs/* +exclude = account/migrations/*,docs/* +inline-quotes = double [tox] envlist = @@ -17,6 +18,7 @@ deps = py{27,33,34,35}: coverage==4.0.2 py32: coverage==3.7.1 flake8 == 3.3.0 + flake8-quotes == 0.11.0 1.8: Django>=1.8,<1.9 1.9: Django>=1.9,<1.10 1.10: Django>=1.10,<1.11 From ea3694235bbc5e56f3f28977eb0d405f1c071c73 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:34:42 -0500 Subject: [PATCH 092/239] Fix single-quote violation --- account/templatetags/account_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 9370a663..18f218cc 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -95,7 +95,7 @@ def urlnext(parser, token): kwargs = {} asvar = None bits = bits[2:] - if len(bits) >= 2 and bits[-2] == 'as': + if len(bits) >= 2 and bits[-2] == "as": asvar = bits[-1] bits = bits[:-2] From c2963e51b6601f222019a659ae324bc713dd6685 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:38:28 -0500 Subject: [PATCH 093/239] Fix copy/paste bug --- README.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f8e1fe93..bfd3984b 100644 --- a/README.rst +++ b/README.rst @@ -19,9 +19,11 @@ Django User Accounts .. image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://pypi.python.org/pypi/django-user-accounts/ -.. image:: https://img.shields.io/github/contributors/pinax/pinax-stripe.svg -.. image:: https://img.shields.io/github/issues-pr/pinax/pinax-stripe.svg -.. image:: https://img.shields.io/github/issues-pr-closed/pinax/pinax-stripe.svg +.. image:: https://img.shields.io/github/contributors/pinax/django-user-accounts.svg + +.. image:: https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg + +.. image:: https://img.shields.io/github/issues-pr-closed/pinax/django-user-accounts.svg Pinax From 23885e6019d5597a6040dab813cf931592f05f6f Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 7 Oct 2017 14:39:44 -0500 Subject: [PATCH 094/239] Link badges to github issues --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index bfd3984b..e95b6565 100644 --- a/README.rst +++ b/README.rst @@ -20,11 +20,11 @@ Django User Accounts :target: https://pypi.python.org/pypi/django-user-accounts/ .. image:: https://img.shields.io/github/contributors/pinax/django-user-accounts.svg - + :target: https://github.com/pinax/django-user-accounts/issues/ .. image:: https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg - + :target: https://github.com/pinax/django-user-accounts/issues/ .. image:: https://img.shields.io/github/issues-pr-closed/pinax/django-user-accounts.svg - + :target: https://github.com/pinax/django-user-accounts/issues/ Pinax ------- From 02c1c84361a26f73e7674d1898ff2622c5dfd003 Mon Sep 17 00:00:00 2001 From: Frank Ludwig <32680873+aibon@users.noreply.github.com> Date: Tue, 17 Oct 2017 19:27:22 +0200 Subject: [PATCH 095/239] Update german translation --- account/locale/de/LC_MESSAGES/django.mo | Bin 3105 -> 3962 bytes account/locale/de/LC_MESSAGES/django.po | 185 +++++++++++++++++------- 2 files changed, 132 insertions(+), 53 deletions(-) diff --git a/account/locale/de/LC_MESSAGES/django.mo b/account/locale/de/LC_MESSAGES/django.mo index 64860b30a37dc3473255169a9bceedf747985c79..880d129adcb3aae68e3432d253add5717570612a 100644 GIT binary patch delta 1898 zcmd7RL2MLN7{Kw*wzResThJinn*QZOA_XkAa-c1?iKNiOC-EFUhhJbX{)`*&2JXO{ z_%L=e*^Pbp81|#ikD%U{!|fssd7VK&6E)n3tGEq6#vFc)d+`ox0Hf_9uiy;of~OPD z;y%tlM7{qizJ!0`aoo|dcHi5m0epp<*^8y3bk`g|SRosg2;!Zq| zn)>gMn#naR;NLiouM+h$cm`j?Pf^#qh5Y0WhlAKjTJ^da)c38niTp1yxX8o-+(uZM zqT{$31Joyf2Q~8dP)qYZ@{^B}^UqLAa~U<&KcbfA8fs~NM-Aw9qF$_->Fgo@dP5Hr zx^X}1M#HEZ??oO`a;Q&IL|teOwKOMCud5{UCy~m?6Lf7_4$ZIzq4y;p6{EG*tiKMQ zN+uN6kQTZ=X@;&%H`k_FdAT_f*_EiN)F59-&J{z~cR4-N@KeH7~TKv^i9l*Z4v*j{&B79Ue&%8_*Ph* zzj+EBb%nBILvO(&oVoU+y(6j2P%1NGGNaky%)ro|)oFngh zz#G$3`L3cH#i_%g>qixr(6VOyNU><9oXNb|m!EVdiaBR&Y$iWDyPr$fo-TFRy49>H zI#Y!sPJ!!+#azWp2Vr@CSCZOj@NOywOLlO#-rCvBOqQgt`W?qE{nBx>wK1_6nnP>l zi>YjE+!JM6aqA1#Pn!c?Oxl;PTwJVJ-^_ZH!1JkO-A4Cr`fs)DO-kD==KrOZ8}HXr F{smmSTigHu delta 1085 zcmZwF&r1|x7{Kv2-O^nxOU<=39n(W(lDvGDOn%fF^##0~oJWsuykCi?g^Bm#`aGu^w+@BW_?TZdS*iq0D=MT}lcuHyKx*xaTIT&9N;;Q;2V^UqU!w`>_uvQ8IOr5~*7_g`0R9Kj9&?V@jRH8I%R@qLlmrp1~(5-+jkH zj8onSxFT!=(!OEvYU+EyJeop;h&g>FU{vYuA*X=$teQfkPw zj+=v)VGpP5q-l*6+5c^zvO=lc;)o>Pl9Ryna^X64w=XDUi idKkLtyqEsZkoHuN^PPodtqR%=D>a$dUOp^nU;P4J%Ay(o diff --git a/account/locale/de/LC_MESSAGES/django.po b/account/locale/de/LC_MESSAGES/django.po index 28c39646..d9a6ae93 100644 --- a/account/locale/de/LC_MESSAGES/django.po +++ b/account/locale/de/LC_MESSAGES/django.po @@ -1,153 +1,232 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# -# Translators: -# Frank , 2014 +# msgid "" msgstr "" "Project-Id-Version: django-user-accounts\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-07-30 15:12-0600\n" -"PO-Revision-Date: 2014-12-05 11:26+0000\n" -"Last-Translator: Frank \n" -"Language-Team: German (http://www.transifex.com/projects/p/django-user-accounts/language/de/)\n" +"POT-Creation-Date: 2017-10-17 18:31+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"accounts/language/de/)\n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: forms.py:27 forms.py:107 +#: account/forms.py:45 account/forms.py:125 msgid "Username" msgstr "Benutzername" -#: forms.py:33 forms.py:79 +#: account/forms.py:51 account/forms.py:140 account/forms.py:186 +#: account/forms.py:215 +msgid "Email" +msgstr "Email" + +#: account/forms.py:55 account/forms.py:97 msgid "Password" msgstr "Passwort" -#: forms.py:37 +#: account/forms.py:59 msgid "Password (again)" msgstr "Passwort (wiederholen)" -#: forms.py:41 forms.py:122 forms.py:168 forms.py:197 -msgid "Email" -msgstr "Email" - -#: forms.py:52 +#: account/forms.py:70 msgid "Usernames can only contain letters, numbers and underscores." msgstr "Der Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten." -#: forms.py:60 +#: account/forms.py:78 msgid "This username is already taken. Please choose another." msgstr "Dieser Benutzername ist bereits vergeben. Bitte wählen sie einen Anderen." -#: forms.py:67 forms.py:217 +#: account/forms.py:85 account/forms.py:235 msgid "A user is registered with this email address." msgstr "Es ist bereits ein Benutzer registriert mit dieser Email Adresse." -#: forms.py:72 forms.py:162 forms.py:191 +#: account/forms.py:90 account/forms.py:180 account/forms.py:209 msgid "You must type the same password each time." msgstr "Die eingegebnen Passwörter stimmen nicht überein." -#: forms.py:83 +#: account/forms.py:101 msgid "Remember Me" msgstr "Auf desem Computer merken" -#: forms.py:96 +#: account/forms.py:114 msgid "This account is inactive." msgstr "Dieses Konto ist nicht aktiv." -#: forms.py:108 +#: account/forms.py:126 msgid "The username and/or password you specified are not correct." msgstr "Die Email Adresse und/oder das angegebene Passwort sind nicht korrekt." -#: forms.py:123 +#: account/forms.py:141 msgid "The email address and/or password you specified are not correct." msgstr "Die Email Adresse und/oder das eingegebene Passwort sind nicht korrekt." -#: forms.py:138 +#: account/forms.py:156 msgid "Current Password" msgstr "Derzeitiges Passwort" -#: forms.py:142 forms.py:180 +#: account/forms.py:160 account/forms.py:198 msgid "New Password" msgstr "Neues Passwort" -#: forms.py:146 forms.py:184 +#: account/forms.py:164 account/forms.py:202 msgid "New Password (again)" msgstr "Neues Passwort (wiederholen)" -#: forms.py:156 +#: account/forms.py:174 msgid "Please type your current password." msgstr "Bitte derzeitiges Passwort eingeben." -#: forms.py:173 +#: account/forms.py:191 msgid "Email address can not be found." msgstr "Email Adresse wurde nicht gefunden." -#: forms.py:199 +#: account/forms.py:217 msgid "Timezone" msgstr "Zeitzone" -#: forms.py:205 +#: account/forms.py:223 msgid "Language" msgstr "Sprache" -#: models.py:34 +#: account/middleware.py:92 +msgid "Your password has expired. Please save a new password." +msgstr "Ihr Passwort ist abgelaufen. Bitte wählen Sie ein neues Passwort." + +#: account/models.py:36 account/models.py:412 msgid "user" -msgstr "benutzer" +msgstr "user" -#: models.py:35 +#: account/models.py:37 msgid "timezone" -msgstr "zeitzone" +msgstr "timezone" -#: models.py:37 +#: account/models.py:39 msgid "language" -msgstr "sprache" +msgstr "language" + +#: account/models.py:140 +msgid "code" +msgstr "code" + +#: account/models.py:141 +msgid "max uses" +msgstr "max uses" + +#: account/models.py:142 +msgid "expiry" +msgstr "expiry" + +#: account/models.py:145 +msgid "notes" +msgstr "notes" + +#: account/models.py:146 +msgid "sent" +msgstr "sent" + +#: account/models.py:147 +msgid "created" +msgstr "created" + +#: account/models.py:148 +msgid "use count" +msgstr "use count" + +#: account/models.py:151 +msgid "signup code" +msgstr "signup code" -#: models.py:250 +#: account/models.py:152 +msgid "signup codes" +msgstr "signup codes" + +#: account/models.py:259 +msgid "verified" +msgstr "verified" + +#: account/models.py:260 +msgid "primary" +msgstr "primary" + +#: account/models.py:265 msgid "email address" -msgstr "email adresse" +msgstr "email address" -#: models.py:251 +#: account/models.py:266 msgid "email addresses" -msgstr "email adressen" +msgstr "email addresses" -#: models.py:300 +#: account/models.py:316 msgid "email confirmation" -msgstr "email bestätigung" +msgstr "email confirmation" -#: models.py:301 +#: account/models.py:317 msgid "email confirmations" -msgstr "email bestätigungen" +msgstr "email confirmations" + +#: account/models.py:366 +msgid "date requested" +msgstr "date requested" + +#: account/models.py:367 +msgid "date expunged" +msgstr "date expunged" + +#: account/models.py:370 +msgid "account deletion" +msgstr "account deletion" + +#: account/models.py:371 +msgid "account deletions" +msgstr "account deletions" + +#: account/models.py:400 +msgid "password history" +msgstr "password history" + +#: account/models.py:401 +msgid "password histories" +msgstr "password histories" -#: views.py:42 +#: account/views.py:54 account/views.py:554 +msgid "Password successfully changed." +msgstr "Passwort wurde gändert." + +#: account/views.py:129 #, python-brace-format msgid "Confirmation email sent to {email}." msgstr "Eine Bestätigungsemail wurde an {email} gesendet." -#: views.py:46 +#: account/views.py:133 #, python-brace-format msgid "The code {code} is invalid." msgstr "Der code {code} ist ungültig." -#: views.py:379 +#: account/views.py:459 #, python-brace-format msgid "You have confirmed {email}." msgstr "Sie haben {email} bestätigt." -#: views.py:452 views.py:585 -msgid "Password successfully changed." -msgstr "Passwort wurde gändert." +#: account/views.py:463 +#, python-brace-format +msgid "Email confirmation for {email} has expired." +msgstr "" -#: views.py:664 +#: account/views.py:729 msgid "Account settings updated." msgstr "Kontoeinstellungen aktualisiert." -#: views.py:748 +#: account/views.py:813 #, python-brace-format msgid "" "Your account is now inactive and your data will be expunged in the next " "{expunge_hours} hours." -msgstr "Ihr Account ist jetzt deaktiviert und Ihre Daten werden in den nächsten {expunge_hours} Stunden endgültig gelöscht." +msgstr "" +"Ihr Account ist jetzt deaktiviert und Ihre Daten werden in den nächsten " +"{expunge_hours} Stunden endgültig gelöscht." From ddb3418a06825a2eca79223b4553c85c50cbf32a Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:30:40 -0500 Subject: [PATCH 096/239] Switch to CircleCI --- .circleci/config.yml | 136 +++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 42 ------------- 2 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .travis.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..3361f43e --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,136 @@ +version: 2.0 + +common: &common + working_directory: ~/repo + steps: + - checkout + - restore_cache: + keys: + - v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} + - v2-deps- + - run: + name: install dependencies + command: pip install --user tox + - run: + name: run tox + command: ~/.local/bin/tox + - run: + name: upload coverage report + command: | + if [[ "$TOXENV" != checkqa ]]; then + PATH=$HOME/.local/bin:$PATH + pip install --user codecov + ~/.local/bin/codecov --required --flags $CIRCLE_JOB + fi + - save_cache: + paths: + - .tox + - ~/.cache/pip + - ~/.local + - ./eggs + key: v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} + +jobs: + lint: + <<: *common + docker: + - image: circleci/python:3.6.1 + environment: + TOXENV=checkqa + py27dj18: + <<: *common + docker: + - image: circleci/python:2.7 + environment: + TOXENV=py27-dj18 + py27dj110: + <<: *common + docker: + - image: circleci/python:2.7 + environment: + TOXENV=py27-dj110 + py27dj111: + <<: *common + docker: + - image: circleci/python:2.7 + environment: + TOXENV=py27-dj111 + py34dj18: + <<: *common + docker: + - image: circleci/python:3.4 + environment: + TOXENV=py34-dj18 + py34dj110: + <<: *common + docker: + - image: circleci/python:3.4 + environment: + TOXENV=py34-dj110 + py34dj111: + <<: *common + docker: + - image: circleci/python:3.4 + environment: + TOXENV=py34-dj111 + py34dj20: + <<: *common + docker: + - image: circleci/python:3.4 + environment: + TOXENV=py34-dj20 + py35dj18: + <<: *common + docker: + - image: circleci/python:3.5 + environment: + TOXENV=py35-dj18 + py35dj110: + <<: *common + docker: + - image: circleci/python:3.5 + environment: + TOXENV=py35-dj110 + py35dj111: + <<: *common + docker: + - image: circleci/python:3.5 + environment: + TOXENV=py35-dj111 + py35dj20: + <<: *common + docker: + - image: circleci/python:3.5 + environment: + TOXENV=py35-dj20 + py36dj111: + <<: *common + docker: + - image: circleci/python:3.6 + environment: + TOXENV=py36-dj111 + py36dj20: + <<: *common + docker: + - image: circleci/python:3.6 + environment: + TOXENV=py36-dj20 + +workflows: + version: 2 + test: + jobs: + - lint + - py27dj18 + - py27dj110 + - py27dj111 + - py34dj18 + - py34dj110 + - py34dj111 + - py34dj20 + - py35dj18 + - py35dj110 + - py35dj111 + - py35dj20 + - py36dj111 + - py36dj20 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e0f1f648..00000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -sudo: false -language: python -python: - - "2.7" - - "3.3" - - "3.4" - - "3.5" - - "3.6" -env: - - DJANGO=1.8 - - DJANGO=1.9 - - DJANGO=1.10 - - DJANGO=1.11 - - DJANGO=master -matrix: - exclude: - - python: "3.3" - env: DJANGO=1.9 - - python: "3.3" - env: DJANGO=1.10 - - python: "3.3" - env: DJANGO=1.11 - - python: "2.7" - env: DJANGO=master - - python: "3.3" - env: DJANGO=master - - python: "3.4" - env: DJANGO=master - - python: "3.6" - env: DJANGO=1.8 - - python: "3.6" - env: DJANGO=1.9 - - python: "3.6" - env: DJANGO=1.10 -install: - - pip install tox coveralls -script: - - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO -after_success: - - coveralls -notifications: - slack: pinax:7G2T4nTnSuv4ZhmJJ3StMM3m From 74122ad32f55e30ea0a8ed1e870331d4f614021b Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:31:39 -0500 Subject: [PATCH 097/239] Update lint configs and test runner --- .coveragerc | 7 ----- account/__init__.py | 5 +++- setup.cfg | 4 +++ setup.py | 7 ++--- tox.ini | 63 +++++++++++++++++++++++++++++++++------------ 5 files changed, 58 insertions(+), 28 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 14a69c4e..00000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -source = account -omit = account/tests/* -branch = 1 - -[report] -omit = account/tests/* diff --git a/account/__init__.py b/account/__init__.py index 5fa9130a..c6f1afd1 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1,4 @@ -__version__ = "2.0.3" +import pkg_resources + + +__version__ = pkg_resources.get_distribution("django-user-accounts").version diff --git a/setup.cfg b/setup.cfg index 2a9acf13..62c9f325 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal = 1 + +[tool:pytest] +testpaths = account/tests +DJANGO_SETTINGS_MODULE = account.tests.settings diff --git a/setup.py b/setup.py index 9ae8aa70..888053ed 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,9 @@ from setuptools import setup, find_packages -import account - setup( name="django-user-accounts", - version=account.__version__, + version="2.0.3", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", @@ -24,6 +22,9 @@ "locale/*/LC_MESSAGES/*", ], }, + extras_require={ + "pytest": ["pytest", "pytest-django"] + }, test_suite="runtests.runtests", classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tox.ini b/tox.ini index 4d5ffa71..21e2fcdf 100644 --- a/tox.ini +++ b/tox.ini @@ -5,30 +5,59 @@ max-complexity = 10 exclude = account/migrations/*,docs/* inline-quotes = double +[isort] +multi_line_output=3 +known_django=django +known_third_party=account,six,mock,appconf,jsonfield +sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +skip_glob=*/account/migrations/* +include_trailing_comma=True + +[coverage:run] +source = account +omit = account/conf.py,account/tests/*,account/migrations/* +branch = true +data_file = .coverage + +[coverage:report] +omit = account/conf.py,account/tests/*,account/migrations/* +exclude_lines = + coverage: omit +show_missing = True + [tox] envlist = - py27-{1.8,1.9,1.10,1.11}, - py33-{1.8}, - py34-{1.8,1.9,1.10,1.11}, - py35-{1.8,1.9,1.10,1.11,master} - py36-{1.11,master} + checkqa + py27-dj{18,110,111}{,-pytest} + py34-dj{18,110,111,20}{,-pytest} + py35-dj{18,110,111,20}{,-pytest} + py36-dj{111,20}{,-pytest} [testenv] +passenv = CI CIRCLECI CIRCLE_* deps = - py{27,33,34,35}: coverage==4.0.2 - py32: coverage==3.7.1 - flake8 == 3.3.0 - flake8-quotes == 0.11.0 - 1.8: Django>=1.8,<1.9 - 1.9: Django>=1.9,<1.10 - 1.10: Django>=1.10,<1.11 - 1.11: Django>=1.11,<1.12 + coverage + codecov + dj18: Django>=1.8,<1.9 + dj110: Django>=1.10,<1.11 + dj111: Django>=1.11a1,<2.0 + dj20: Django<2.1 master: https://github.com/django/django/tarball/master +extras = + pytest: pytest usedevelop = True setenv = - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=en_US.UTF-8 + DJANGO_SETTINGS_MODULE=account.tests.settings + pytest: _ACCOUNT_TEST_RUNNER=-m pytest +commands = + coverage run {env:_ACCOUNT_ TEST_RUNNER:setup.py test} {posargs} + coverage report -m --skip-covered + +[testenv:checkqa] commands = flake8 account - coverage run setup.py test + isort --recursive --check-only --diff account -sp tox.ini +deps = + flake8 == 3.4.1 + flake8-quotes == 0.11.0 + isort == 4.2.15 From 9e870539f999e336558f1c62e43be3dc0914a4ab Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:32:47 -0500 Subject: [PATCH 098/239] Update CONTRIBUTING with new importing ordering --- CONTRIBUTING.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e807a229..4e1c01db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,10 @@ # How to Contribute There are many ways you can help contribute to django-user-accounts. -Contributing code, writing documentation, translations, reporting bugs, as well -as reading and providing feedback on issues and pull requests, all are valid and -necessary ways to help. + +Contributing code, writing documentation, reporting bugs, as well as +reading and providing feedback on issues and pull requests, all are +valid and necessary ways to help. ## Committing Code @@ -70,7 +71,6 @@ Django's coding style: * Use double quotes not single quotes. Single quotes are allowed in cases where a double quote is needed in the string. This makes the code read cleaner in those cases. -* Blank lines should contain no whitespace. * Docstrings always use three double quotes on a line of their own, so, for example, a single line docstring should take up three lines not one. * Imports are grouped specifically and ordered alphabetically. This is shown @@ -86,15 +86,13 @@ Here is an example of these rules applied: # non-from imports go first then from style import in their own group import csv - # second set of imports are Django imports with contrib in their own - # group. - from django.core.urlresolvers import reverse + # second set of imports are Django imports + from django.contrib.auth.models import User from django.db import models + from django.urls import reverse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ - from django.contrib.auth.models import User - # third set of imports are external apps (if applicable) from tagging.fields import TagField From 78f5ba4c122dfdd2fa667000695f9c2a3dff5429 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:33:08 -0500 Subject: [PATCH 099/239] Ignore some tox generated things --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 058eb4d5..0d1d86dd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.egg-info build/ dist/ +.eggs +.tox From f66af11e301a1d7df0cf4cbd3724bf74579da5db Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:33:30 -0500 Subject: [PATCH 100/239] Pull test settings off into own module --- account/tests/settings.py | 48 +++++++++++++++++++++++++++ runtests.py | 69 ++------------------------------------- 2 files changed, 51 insertions(+), 66 deletions(-) create mode 100644 account/tests/settings.py diff --git a/account/tests/settings.py b/account/tests/settings.py new file mode 100644 index 00000000..5186f65b --- /dev/null +++ b/account/tests/settings.py @@ -0,0 +1,48 @@ +DEBUG = True +USE_TZ = True +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "account", + "account.tests", +] +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} +SITE_ID = 1 +ROOT_URLCONF = "account.tests.urls" +SECRET_KEY = "notasecret" +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + # insert your TEMPLATE_DIRS here + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this + # list if you haven"t customized them: + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] +MIDDLEWARE = [ + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware" +] +MIDDLEWARE_CLASSES = MIDDLEWARE diff --git a/runtests.py b/runtests.py index ccd776f2..e64d90ec 100644 --- a/runtests.py +++ b/runtests.py @@ -4,73 +4,9 @@ import django -from django.conf import settings - - -DEFAULT_SETTINGS = dict( - DEBUG=True, - USE_TZ=True, - INSTALLED_APPS=[ - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "account", - "account.tests", - ], - DATABASES={ - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": ":memory:", - } - }, - SITE_ID=1, - ROOT_URLCONF="account.tests.urls", - SECRET_KEY="notasecret", - TEMPLATES=[ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - # insert your TEMPLATE_DIRS here - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this - # list if you haven't customized them: - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, - ] -) - -if django.VERSION >= (1, 10): - DEFAULT_SETTINGS["MIDDLEWARE"] = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - ] -else: - DEFAULT_SETTINGS["MIDDLEWARE_CLASSES"] = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - ] - def runtests(*test_args): - if not settings.configured: - settings.configure(**DEFAULT_SETTINGS) - + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") django.setup() parent = os.path.dirname(os.path.abspath(__file__)) @@ -79,7 +15,8 @@ def runtests(*test_args): try: from django.test.runner import DiscoverRunner runner_class = DiscoverRunner - test_args = ["account.tests"] + if not test_args: + test_args = ["account.tests"] except ImportError: from django.test.simple import DjangoTestSuiteRunner runner_class = DjangoTestSuiteRunner From 44434ad9d4547b02682b3e420fa92cefb2fa7c6e Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:41:12 -0500 Subject: [PATCH 101/239] isort the imports --- account/auth_backends.py | 3 +- account/conf.py | 7 ++-- account/forms.py | 18 +++++----- .../management/commands/expunge_deleted.py | 3 +- .../commands/user_password_history.py | 3 +- account/middleware.py | 18 +++++----- account/models.py | 27 +++++++------- account/signals.py | 1 - account/templatetags/account_tags.py | 1 - account/tests/test_auth.py | 3 +- account/tests/test_commands.py | 10 ++---- account/tests/test_password.py | 20 +++-------- account/tests/test_views.py | 5 ++- account/tests/urls.py | 1 - account/timezones.py | 1 - account/urls.py | 16 ++++++--- account/utils.py | 17 +++++---- account/views.py | 35 ++++++++++++------- 18 files changed, 97 insertions(+), 92 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index c8c4320f..40153d5e 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals -from django.db.models import Q - from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend +from django.db.models import Q from account.models import EmailAddress from account.utils import get_user_lookup_kwargs diff --git a/account/conf.py b/account/conf.py index 4e620af2..654e8056 100644 --- a/account/conf.py +++ b/account/conf.py @@ -6,12 +6,11 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.translation import get_language_info -import pytz - +from account.languages import LANGUAGES +from account.timezones import TIMEZONES from appconf import AppConf -from account.timezones import TIMEZONES -from account.languages import LANGUAGES +import pytz def load_path_attr(path): diff --git a/account/forms.py b/account/forms.py index efbf24a3..37c3ea8d 100644 --- a/account/forms.py +++ b/account/forms.py @@ -2,23 +2,25 @@ import re -try: - from collections import OrderedDict -except ImportError: - OrderedDict = None - from django import forms -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ - from django.contrib import auth from django.contrib.auth import get_user_model +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ from account.conf import settings from account.hooks import hookset from account.models import EmailAddress from account.utils import get_user_lookup_kwargs +try: + from collections import OrderedDict +except ImportError: + OrderedDict = None + + + + alnum_re = re.compile(r"^\w+$") diff --git a/account/management/commands/expunge_deleted.py b/account/management/commands/expunge_deleted.py index d1746492..a424cc13 100644 --- a/account/management/commands/expunge_deleted.py +++ b/account/management/commands/expunge_deleted.py @@ -1,5 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import print_function, unicode_literals from django.core.management.base import BaseCommand diff --git a/account/management/commands/user_password_history.py b/account/management/commands/user_password_history.py index 4349416f..a0232726 100644 --- a/account/management/commands/user_password_history.py +++ b/account/management/commands/user_password_history.py @@ -1,11 +1,12 @@ import datetime -import pytz from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from account.models import PasswordHistory +import pytz + class Command(BaseCommand): diff --git a/account/middleware.py b/account/middleware.py index f42dde18..05a1e3ff 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,25 +1,27 @@ from __future__ import unicode_literals -try: - from urllib.parse import urlparse, urlunparse -except ImportError: # python 2 - from urlparse import urlparse, urlunparse - import django - from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect, QueryDict -from django.utils import translation, timezone +from django.utils import timezone, translation from django.utils.cache import patch_vary_headers from django.utils.translation import ugettext_lazy as _ from account import signals -from account.compat import resolve, reverse, is_authenticated +from account.compat import is_authenticated, resolve, reverse from account.conf import settings from account.models import Account from account.utils import check_password_expired +try: + from urllib.parse import urlparse, urlunparse +except ImportError: # python 2 + from urlparse import urlparse, urlunparse + + + + if django.VERSION >= (1, 10): from django.utils.deprecation import MiddlewareMixin as BaseMiddleware diff --git a/account/models.py b/account/models.py index ef3e08bc..dd13ff18 100644 --- a/account/models.py +++ b/account/models.py @@ -3,32 +3,35 @@ import datetime import operator -try: - from urllib.parse import urlencode -except ImportError: # python 2 - from urllib import urlencode - +from django.contrib.auth.models import AnonymousUser +from django.contrib.sites.models import Site from django.db import models, transaction from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver -from django.utils import timezone, translation, six +from django.utils import six, timezone, translation from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import AnonymousUser -from django.contrib.sites.models import Site - -import pytz - from account import signals -from account.compat import reverse, is_authenticated +from account.compat import is_authenticated, reverse from account.conf import settings from account.fields import TimeZoneField from account.hooks import hookset from account.managers import EmailAddressManager, EmailConfirmationManager from account.signals import signup_code_sent, signup_code_used +import pytz + +try: + from urllib.parse import urlencode +except ImportError: # python 2 + from urllib import urlencode + + + + + @python_2_unicode_compatible class Account(models.Model): diff --git a/account/signals.py b/account/signals.py index 8bc14a96..40c7dbdd 100644 --- a/account/signals.py +++ b/account/signals.py @@ -2,7 +2,6 @@ import django.dispatch - user_signed_up = django.dispatch.Signal(providing_args=["user", "form"]) user_sign_up_attempt = django.dispatch.Signal(providing_args=["username", "email", "result"]) user_logged_in = django.dispatch.Signal(providing_args=["user", "form"]) diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 18f218cc..1765bdba 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -8,7 +8,6 @@ from account.utils import user_display - register = template.Library() diff --git a/account/tests/test_auth.py b/account/tests/test_auth.py index 8f852a48..a4538dc0 100644 --- a/account/tests/test_auth.py +++ b/account/tests/test_auth.py @@ -1,7 +1,6 @@ -from django.test import TestCase, override_settings - from django.contrib.auth import authenticate from django.contrib.auth.models import User +from django.test import TestCase, override_settings @override_settings( diff --git a/account/tests/test_commands.py b/account/tests/test_commands.py index 594c9e71..a67ae375 100644 --- a/account/tests/test_commands.py +++ b/account/tests/test_commands.py @@ -1,16 +1,10 @@ from django.contrib.auth import get_user_model from django.core.management import call_command -from django.test import ( - override_settings, - TestCase, -) +from django.test import TestCase, override_settings from django.utils.six import StringIO from ..conf import settings -from ..models import ( - PasswordExpiry, - PasswordHistory, -) +from ..models import PasswordExpiry, PasswordHistory @override_settings( diff --git a/account/tests/test_password.py b/account/tests/test_password.py index c489216b..21367476 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -1,24 +1,14 @@ import datetime -import pytz import django - -from django.contrib.auth.hashers import ( - check_password, - make_password, -) +from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.models import User -from django.test import ( - TestCase, - modify_settings, - override_settings, -) +from django.test import TestCase, modify_settings, override_settings + +import pytz from ..compat import reverse -from ..models import ( - PasswordExpiry, - PasswordHistory, -) +from ..models import PasswordExpiry, PasswordHistory from ..utils import check_password_expired diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 1247691d..83ffc067 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -1,13 +1,12 @@ from django.conf import settings +from django.contrib.auth.models import User from django.core import mail from django.test import TestCase, override_settings from django.utils.http import int_to_base36 from django.utils.six.moves.urllib.parse import urlparse -from django.contrib.auth.models import User - from account.compat import reverse -from account.models import SignupCode, EmailConfirmation +from account.models import EmailConfirmation, SignupCode class SignupViewTestCase(TestCase): diff --git a/account/tests/urls.py b/account/tests/urls.py index 679e33aa..a2c9a2f4 100644 --- a/account/tests/urls.py +++ b/account/tests/urls.py @@ -1,6 +1,5 @@ from django.conf.urls import include, url - urlpatterns = [ url(r"^", include("account.urls")), ] diff --git a/account/timezones.py b/account/timezones.py index 20946852..cbc1e014 100644 --- a/account/timezones.py +++ b/account/timezones.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals - TIMEZONES = [ ("Africa/Abidjan", "Africa/Abidjan"), ("Africa/Accra", "Africa/Accra"), diff --git a/account/urls.py b/account/urls.py index c5c1de3e..bbd2d17e 100644 --- a/account/urls.py +++ b/account/urls.py @@ -2,11 +2,17 @@ from django.conf.urls import url -from account.views import SignupView, LoginView, LogoutView, DeleteView -from account.views import ConfirmEmailView -from account.views import ChangePasswordView, PasswordResetView, PasswordResetTokenView -from account.views import SettingsView - +from account.views import ( + ChangePasswordView, + ConfirmEmailView, + DeleteView, + LoginView, + LogoutView, + PasswordResetTokenView, + PasswordResetView, + SettingsView, + SignupView, +) urlpatterns = [ url(r"^signup/$", SignupView.as_view(), name="account_signup"), diff --git a/account/utils.py b/account/utils.py index 6d3078f3..dc95c51a 100644 --- a/account/utils.py +++ b/account/utils.py @@ -2,20 +2,25 @@ import datetime import functools + +from django.contrib.auth import get_user_model +from django.core.exceptions import SuspiciousOperation +from django.http import HttpResponseRedirect, QueryDict + +from account.compat import NoReverseMatch, reverse +from account.conf import settings + import pytz + +from .models import PasswordHistory + try: from urllib.parse import urlparse, urlunparse except ImportError: # python 2 from urlparse import urlparse, urlunparse -from django.core.exceptions import SuspiciousOperation -from django.http import HttpResponseRedirect, QueryDict -from django.contrib.auth import get_user_model -from account.compat import reverse, NoReverseMatch -from account.conf import settings -from .models import PasswordHistory def get_user_lookup_kwargs(kwargs): diff --git a/account/views.py b/account/views.py index bdf8e09f..6c73e932 100644 --- a/account/views.py +++ b/account/views.py @@ -1,7 +1,12 @@ from __future__ import unicode_literals +from django.contrib import auth, messages +from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.shortcuts import get_current_site from django.http import Http404, HttpResponseForbidden -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator from django.utils.http import base36_to_int, int_to_base36 from django.utils.translation import ugettext_lazy as _ @@ -11,21 +16,27 @@ from django.views.generic.base import TemplateResponseMixin, View from django.views.generic.edit import FormView -from django.contrib import auth, messages -from django.contrib.auth import get_user_model -from django.contrib.auth.hashers import make_password -from django.contrib.auth.tokens import default_token_generator -from django.contrib.sites.shortcuts import get_current_site - from account import signals -from account.compat import reverse, is_authenticated +from account.compat import is_authenticated, reverse from account.conf import settings -from account.forms import SignupForm, LoginUsernameForm -from account.forms import ChangePasswordForm, PasswordResetForm, PasswordResetTokenForm -from account.forms import SettingsForm +from account.forms import ( + ChangePasswordForm, + LoginUsernameForm, + PasswordResetForm, + PasswordResetTokenForm, + SettingsForm, + SignupForm, +) from account.hooks import hookset from account.mixins import LoginRequiredMixin -from account.models import SignupCode, EmailAddress, EmailConfirmation, Account, AccountDeletion, PasswordHistory +from account.models import ( + Account, + AccountDeletion, + EmailAddress, + EmailConfirmation, + PasswordHistory, + SignupCode, +) from account.utils import default_redirect, get_form_data From 8acea6f13125c2f24e50ef6b06226b6d3c0297b7 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:45:30 -0500 Subject: [PATCH 102/239] Lints post isort --- account/forms.py | 3 --- account/middleware.py | 3 --- account/models.py | 4 ---- account/utils.py | 3 --- 4 files changed, 13 deletions(-) diff --git a/account/forms.py b/account/forms.py index 37c3ea8d..9c5f9b0e 100644 --- a/account/forms.py +++ b/account/forms.py @@ -19,9 +19,6 @@ OrderedDict = None - - - alnum_re = re.compile(r"^\w+$") diff --git a/account/middleware.py b/account/middleware.py index 05a1e3ff..e3216239 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -20,9 +20,6 @@ from urlparse import urlparse, urlunparse - - - if django.VERSION >= (1, 10): from django.utils.deprecation import MiddlewareMixin as BaseMiddleware else: diff --git a/account/models.py b/account/models.py index dd13ff18..549c2e1e 100644 --- a/account/models.py +++ b/account/models.py @@ -29,10 +29,6 @@ from urllib import urlencode - - - - @python_2_unicode_compatible class Account(models.Model): diff --git a/account/utils.py b/account/utils.py index dc95c51a..ea0b2696 100644 --- a/account/utils.py +++ b/account/utils.py @@ -20,9 +20,6 @@ from urlparse import urlparse, urlunparse - - - def get_user_lookup_kwargs(kwargs): result = {} username_field = getattr(get_user_model(), "USERNAME_FIELD", "username") From 31e9eadd66bb3075999e8d69b81192ae024f38b3 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:47:52 -0500 Subject: [PATCH 103/239] Update badges --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e95b6565..99b22901 100644 --- a/README.rst +++ b/README.rst @@ -7,11 +7,11 @@ Django User Accounts .. image:: http://slack.pinaxproject.com/badge.svg :target: http://slack.pinaxproject.com/ -.. image:: https://img.shields.io/travis/pinax/django-user-accounts.svg - :target: https://travis-ci.org/pinax/django-user-accounts +.. image:: https://circleci.com/gh/pinax/django-user-accounts.svg?style=svg + :target: https://circleci.com/gh/pinax/django-user-accounts -.. image:: https://img.shields.io/coveralls/pinax/django-user-accounts.svg - :target: https://coveralls.io/r/pinax/django-user-accounts +.. image:: https://img.shields.io/codecov/c/github/pinax/django-user-accounts.svg + :target: https://codecov.io/gh/pinax/django-user-accounts .. image:: https://img.shields.io/pypi/v/django-user-accounts.svg :target: https://pypi.python.org/pypi/django-user-accounts/ From b26cfdb5f3c4ec4ef085741799bd863593a2d6e1 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:49:25 -0500 Subject: [PATCH 104/239] Add pytz as a known third party --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 21e2fcdf..662ae4fc 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ inline-quotes = double [isort] multi_line_output=3 known_django=django -known_third_party=account,six,mock,appconf,jsonfield +known_third_party=account,six,mock,appconf,jsonfield,pytz sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER skip_glob=*/account/migrations/* include_trailing_comma=True From d88cc2853185be84039f9295487618bc6d7d8951 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Oct 2017 15:56:00 -0500 Subject: [PATCH 105/239] Fix up imports after marking pytz as known third party --- account/conf.py | 3 +-- account/management/commands/user_password_history.py | 3 +-- account/models.py | 3 +-- account/utils.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/account/conf.py b/account/conf.py index 654e8056..8d08153e 100644 --- a/account/conf.py +++ b/account/conf.py @@ -6,12 +6,11 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.translation import get_language_info +import pytz from account.languages import LANGUAGES from account.timezones import TIMEZONES from appconf import AppConf -import pytz - def load_path_attr(path): i = path.rfind(".") diff --git a/account/management/commands/user_password_history.py b/account/management/commands/user_password_history.py index a0232726..bffc452b 100644 --- a/account/management/commands/user_password_history.py +++ b/account/management/commands/user_password_history.py @@ -3,9 +3,8 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from account.models import PasswordHistory - import pytz +from account.models import PasswordHistory class Command(BaseCommand): diff --git a/account/models.py b/account/models.py index 549c2e1e..4dc59147 100644 --- a/account/models.py +++ b/account/models.py @@ -13,6 +13,7 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +import pytz from account import signals from account.compat import is_authenticated, reverse from account.conf import settings @@ -21,8 +22,6 @@ from account.managers import EmailAddressManager, EmailConfirmationManager from account.signals import signup_code_sent, signup_code_used -import pytz - try: from urllib.parse import urlencode except ImportError: # python 2 diff --git a/account/utils.py b/account/utils.py index ea0b2696..5f2ae8b2 100644 --- a/account/utils.py +++ b/account/utils.py @@ -7,11 +7,10 @@ from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict +import pytz from account.compat import NoReverseMatch, reverse from account.conf import settings -import pytz - from .models import PasswordHistory try: From dc1f12953944321d4059407f4d0cf25e5544b410 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Sat, 28 Oct 2017 17:27:58 -0600 Subject: [PATCH 106/239] Fix tox.ini typo, update README requirements --- README.rst | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 99b22901..6fe133bc 100644 --- a/README.rst +++ b/README.rst @@ -63,8 +63,8 @@ Features Requirements -------------- -* Django 1.8, 1.9, 1.10 and 1.11 -* Python 2.7, 3.3, 3.4, 3.5 and 3.6 +* Django 1.8, 1.10, 1.11, or 2.0 +* Python 2.7, 3.4, 3.5, or 3.6 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) diff --git a/tox.ini b/tox.ini index 662ae4fc..a8235a38 100644 --- a/tox.ini +++ b/tox.ini @@ -50,7 +50,7 @@ setenv = DJANGO_SETTINGS_MODULE=account.tests.settings pytest: _ACCOUNT_TEST_RUNNER=-m pytest commands = - coverage run {env:_ACCOUNT_ TEST_RUNNER:setup.py test} {posargs} + coverage run {env:_ACCOUNT_TEST_RUNNER:setup.py test} {posargs} coverage report -m --skip-covered [testenv:checkqa] From 490a8fda848b2630d93151dcc4539627b3eceff1 Mon Sep 17 00:00:00 2001 From: Shosh Seiden Date: Sat, 6 Jan 2018 15:06:19 -0700 Subject: [PATCH 107/239] Edited comment for replacing one-to-one profile attribute by specifying to only change the name if a related_name was defined. --- docs/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.rst b/docs/usage.rst index edcd3859..b0abb043 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -46,7 +46,7 @@ defined in your project:: super(SignupView, self).after_signup(form) def update_profile(self, form): - profile = self.created_user.profile # replace with your reverse one-to-one profile attribute + profile = self.created_user.profile # replace with your reverse one-to-one profile attribute only if you've defined a `related_name`. profile.some_attr = "some value" profile.save() From 3ea6f37ed5efc79a4ebd312b5b5ae79d142e9038 Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Tue, 20 Feb 2018 13:30:30 +0200 Subject: [PATCH 108/239] If newly-created user cannot be authenticated, blame configuration In case a newly-created user cannot be authenticated, this means there's a mismatch between the fields used for creation and the authentication backends. --- account/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/account/views.py b/account/views.py index 6c73e932..12fc2cbe 100644 --- a/account/views.py +++ b/account/views.py @@ -5,6 +5,7 @@ from django.contrib.auth.hashers import make_password from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ImproperlyConfigured from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator @@ -301,6 +302,8 @@ def after_signup(self, form): def login_user(self): user = auth.authenticate(**self.user_credentials()) + if not user: + raise ImproperlyConfigured("Configured auth backends failed to authenticate on signup") auth.login(self.request, user) self.request.session.set_expiry(0) From a1352717c2d7126102b766de95b8347f2438020e Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 11 Mar 2018 18:52:05 -0700 Subject: [PATCH 109/239] Change 'and' to 'or' Change the 'and' in these sentences to 'or' to express that not all versions are needed as requirements. Only one version is required. Add a comma before 'or' to improve the serial nature of the list, specifically the last two items. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 99b22901..a239e6df 100644 --- a/README.rst +++ b/README.rst @@ -63,8 +63,8 @@ Features Requirements -------------- -* Django 1.8, 1.9, 1.10 and 1.11 -* Python 2.7, 3.3, 3.4, 3.5 and 3.6 +* Django 1.8, 1.9, 1.10, or 1.11 +* Python 2.7, 3.3, 3.4, 3.5, or 3.6 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) From 04b6988fbe20cb1ed85bdd9baa4a97740e147a4c Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 11 Mar 2018 18:57:49 -0700 Subject: [PATCH 110/239] Update mention of Pinax Hangout Change the mention from "recently" to a specific time, given the hangout was two years ago. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a239e6df..f85aa2c7 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Documentation ---------------- See http://django-user-accounts.readthedocs.org/ for the ``django-user-accounts`` documentation. -We recently did a Pinax Hangout on ``django-user-accounts``, you can read the recap blog post and find the video here http://blog.pinaxproject.com/2015/10/12/recap-september-pinax-hangout/. +On September 17th, 2015, we did a Pinax Hangout on ``django-user-accounts``. You can read the recap blog post and find the video here http://blog.pinaxproject.com/2015/10/12/recap-september-pinax-hangout/. The Pinax documentation is available at http://pinaxproject.com/pinax/. If you would like to help us improve our documentation or write more documentation, please join our Slack team and let us know! From 5a1dcff49bb4362e328f26e9acc2b0d723d1d7af Mon Sep 17 00:00:00 2001 From: Ryan Nowakowski Date: Thu, 23 Aug 2018 23:30:49 -0500 Subject: [PATCH 111/239] allow request arg to be passed to authenticate Django 2.1 mandates that the first parameter passed to backend.authenticate() must be request. Without this, login simply fails as if you had entered a wrong username/password. Use *args to allow request to be passed in for Django 2.1 compatibility. --- account/auth_backends.py | 4 ++-- account/tests/test_auth.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index 40153d5e..b4ae5863 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -10,7 +10,7 @@ class UsernameAuthenticationBackend(ModelBackend): - def authenticate(self, **credentials): + def authenticate(self, *args, **credentials): User = get_user_model() try: lookup_kwargs = get_user_lookup_kwargs({ @@ -29,7 +29,7 @@ def authenticate(self, **credentials): class EmailAuthenticationBackend(ModelBackend): - def authenticate(self, **credentials): + def authenticate(self, *args, **credentials): qs = EmailAddress.objects.filter(Q(primary=True) | Q(verified=True)) try: email_address = qs.get(email__iexact=credentials["username"]) diff --git a/account/tests/test_auth.py b/account/tests/test_auth.py index a4538dc0..e639849b 100644 --- a/account/tests/test_auth.py +++ b/account/tests/test_auth.py @@ -29,6 +29,18 @@ def test_missing_credentials(self): self.assertTrue(authenticate() is None) self.assertTrue(authenticate(username="user1") is None) + def test_successful_auth_django_2_1(self): + created_user = self.create_user("user1", "user1@example.com", "password") + request = None + authed_user = authenticate(request, username="user1", password="password") + self.assertTrue(authed_user is not None) + self.assertEqual(created_user.pk, authed_user.pk) + + def test_unsuccessful_auth_django_2_1(self): + request = None + authed_user = authenticate(request, username="user-does-not-exist", password="password") + self.assertTrue(authed_user is None) + @override_settings( AUTHENTICATION_BACKENDS=[ @@ -55,3 +67,15 @@ def test_missing_credentials(self): self.create_user("user1", "user1@example.com", "password") self.assertTrue(authenticate() is None) self.assertTrue(authenticate(username="user1@example.com") is None) + + def test_successful_auth_django_2_1(self): + created_user = self.create_user("user1", "user1@example.com", "password") + request = None + authed_user = authenticate(request, username="user1@example.com", password="password") + self.assertTrue(authed_user is not None) + self.assertEqual(created_user.pk, authed_user.pk) + + def test_unsuccessful_auth_django_2_1(self): + request = None + authed_user = authenticate(request, username="user-does-not-exist", password="password") + self.assertTrue(authed_user is None) From 865a1703062e97a9d1c95a48c8a1534cf674e268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Br=C3=BCck?= Date: Thu, 15 Nov 2018 17:56:53 +0100 Subject: [PATCH 112/239] fix typo in german translation --- account/locale/de/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/locale/de/LC_MESSAGES/django.po b/account/locale/de/LC_MESSAGES/django.po index 28c39646..a42084df 100644 --- a/account/locale/de/LC_MESSAGES/django.po +++ b/account/locale/de/LC_MESSAGES/django.po @@ -52,7 +52,7 @@ msgstr "Die eingegebnen Passwörter stimmen nicht überein." #: forms.py:83 msgid "Remember Me" -msgstr "Auf desem Computer merken" +msgstr "Auf diesem Computer merken" #: forms.py:96 msgid "This account is inactive." From 304101485fed8bc210a1cc9fb4beb027f6f64280 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 19:22:05 -0600 Subject: [PATCH 113/239] Only support 1.11 and above --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 888053ed..3ad3721d 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ url="http://github.com/pinax/django-user-accounts", packages=find_packages(), install_requires=[ - "Django>=1.8", + "Django>=1.11", "django-appconf>=1.0.1", "pytz>=2015.6" ], From 89dc8a09a3647f9f908727c18224c58ddcbf1fe4 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 19:23:11 -0600 Subject: [PATCH 114/239] Only support 1.11 and above --- tox.ini | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index a8235a38..f56b6722 100644 --- a/tox.ini +++ b/tox.ini @@ -28,9 +28,9 @@ show_missing = True [tox] envlist = checkqa - py27-dj{18,110,111}{,-pytest} - py34-dj{18,110,111,20}{,-pytest} - py35-dj{18,110,111,20}{,-pytest} + py27-dj{111}{,-pytest} + py34-dj{111,20}{,-pytest} + py35-dj{111,20}{,-pytest} py36-dj{111,20}{,-pytest} [testenv] @@ -38,9 +38,7 @@ passenv = CI CIRCLECI CIRCLE_* deps = coverage codecov - dj18: Django>=1.8,<1.9 - dj110: Django>=1.10,<1.11 - dj111: Django>=1.11a1,<2.0 + dj111: Django>=1.11,<2.0 dj20: Django<2.1 master: https://github.com/django/django/tarball/master extras = From 054c6e0181a712b44e1574174803479bf0ae67ac Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 19:23:57 -0600 Subject: [PATCH 115/239] Only support 1.11 and above --- .circleci/config.yml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3361f43e..992a6751 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -37,36 +37,12 @@ jobs: - image: circleci/python:3.6.1 environment: TOXENV=checkqa - py27dj18: - <<: *common - docker: - - image: circleci/python:2.7 - environment: - TOXENV=py27-dj18 - py27dj110: - <<: *common - docker: - - image: circleci/python:2.7 - environment: - TOXENV=py27-dj110 py27dj111: <<: *common docker: - image: circleci/python:2.7 environment: TOXENV=py27-dj111 - py34dj18: - <<: *common - docker: - - image: circleci/python:3.4 - environment: - TOXENV=py34-dj18 - py34dj110: - <<: *common - docker: - - image: circleci/python:3.4 - environment: - TOXENV=py34-dj110 py34dj111: <<: *common docker: @@ -79,18 +55,6 @@ jobs: - image: circleci/python:3.4 environment: TOXENV=py34-dj20 - py35dj18: - <<: *common - docker: - - image: circleci/python:3.5 - environment: - TOXENV=py35-dj18 - py35dj110: - <<: *common - docker: - - image: circleci/python:3.5 - environment: - TOXENV=py35-dj110 py35dj111: <<: *common docker: @@ -121,15 +85,9 @@ workflows: test: jobs: - lint - - py27dj18 - - py27dj110 - py27dj111 - - py34dj18 - - py34dj110 - py34dj111 - py34dj20 - - py35dj18 - - py35dj110 - py35dj111 - py35dj20 - py36dj111 From c6570bab2ebaa57f247073a7ef594386368be4c8 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 20:23:49 -0600 Subject: [PATCH 116/239] Fix lint issue --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index 83802c6f..cdff0f13 100644 --- a/account/views.py +++ b/account/views.py @@ -676,7 +676,7 @@ def dispatch(self, *args, **kwargs): if user is not None: token = kwargs["token"] if token == INTERNAL_RESET_URL_TOKEN: - session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN, '') + session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN, "") if self.check_token(user, session_token): return super(PasswordResetTokenView, self).dispatch(*args, **kwargs) else: From a61518c9d05c1faa7938d4682f446ef757116ba2 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 20:24:55 -0600 Subject: [PATCH 117/239] 2.1.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ad3721d..b4238c68 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-user-accounts", - version="2.0.3", + version="2.1.0", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From ad301bd57ae65c456ed6e45ae3b5cb06d4e2378b Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 30 Nov 2018 20:26:16 -0600 Subject: [PATCH 118/239] Fix import order --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index 9a47ed47..70d78cd9 100644 --- a/account/models.py +++ b/account/models.py @@ -19,9 +19,9 @@ from account.conf import settings from account.fields import TimeZoneField from account.hooks import hookset +from account.languages import DEFAULT_LANGUAGE from account.managers import EmailAddressManager, EmailConfirmationManager from account.signals import signup_code_sent, signup_code_used -from account.languages import DEFAULT_LANGUAGE try: from urllib.parse import urlencode From 433c6792737ce431f1c162ed96bd75cb18c32516 Mon Sep 17 00:00:00 2001 From: Dmitry Shorokhov Date: Sat, 19 Jan 2019 02:31:39 +0300 Subject: [PATCH 119/239] russian localization update --- account/locale/ru/LC_MESSAGES/django.mo | Bin 3015 -> 3711 bytes account/locale/ru/LC_MESSAGES/django.po | 22 ++++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/account/locale/ru/LC_MESSAGES/django.mo b/account/locale/ru/LC_MESSAGES/django.mo index ad65e19a941fc30fc5ea404bb265aee6c89cc08f..c4c90a582fe14ccbf5a9085c8487fde59953f3af 100644 GIT binary patch delta 1550 zcma))%WqUw9LG-|N@t27rBJk^obm`Dw8RA_rX-p)UHJ$L2owV3GWV8VGQF3~+|m*# zeTAUX7~_@1=%SI88WV#SZxV?wwkQ8_(pN&$(xQ&+j>R zT3%#E9yHZoQH(ZbhPkCisoii_Eeqps*bX1UcVJ7MQk&t2@NIYiz5#pTMwo*Qa1ySB zS70-|T0Or3#qUq}zEUG9VzY^Zr?3&OtyiiQZiJu0PS^?0Ln)vFKZ5t6BubH8*aY{& zRw(|5;0G{($KVuv3qFUd;mSAhA-~G7SFh52 z3^pLsPPiYQhNDmt`~&3*QbfN9cR*3@8axOe!=GUnGRPJD1xK3LJY}O{-KvCJpj6%s zCGeMU1MGze5f8+OlgXQr@g7rxViI2yh}9FeF-3Z5O5(_n#u%s6yR2MEd<84udZuQ| zKp4fdP;2Cau`WTp((J#3x2X^(mQ@brex-03^7y2grm(;Eiyq(Wb@TmJ;QF3+`Yku7 z3yv4)z}M&E&*2W$HIUEC**>dK81(bD>K40X)s}7NokBrpEl+!Xpielu*B|igj__XX zk1bi>cJw(}hqYVKu6M@Dxpv3QiMmZS>-(%Tj?N|pIQCM$@Irn2(H?&wudQsBNP#$d z{vgg)HgIvVJX@a?1KSELJ?Q3glGPbHJ>Z=rj_c{5&(WSU6c95xJ>G|XVOYnQ`K|s* z?dmC0iN>NyGiMfbG^WE`Rj>T%*IbhRiBurc9B53+7fdDM!B^ zF+cHDT2fG)DWZ(cJk{MylDrnaLWvbAZ#huxHA@?#QmmP!9lSfj?Pvckuq6rDYhJWJgiY{Z%6~|xl_qWm4 W1S_}x=XFGv2s=iF)8W>}t1kf*4%^KD delta 869 zcma*lO-vI(6u|M0&c;lsHX^xppcD7 z^Q(#LGx_GmkO(<6${s;24|@tn_b=M$X*uG9#wF*6%{P#OsB2$(zJ&eD4L zlM}dS#uK|E{_R5ixOp4zqGgPmnCop!>b6B&_NHycn{jRRu&vr9TXpSATV$|im%KUG zKI2odW!kd&oJa?4?U0Ej-H!j6U~4+Xv9;3qroMn Date: Tue, 14 May 2019 19:51:50 +0900 Subject: [PATCH 120/239] Bugfix: Internal error on Password Reset --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index cdff0f13..a212762e 100644 --- a/account/views.py +++ b/account/views.py @@ -706,7 +706,7 @@ def get_context_data(self, **kwargs): def form_valid(self, form): self.change_password(form) - self.create_password_history(form, self.request.user) + self.create_password_history(form, self.get_user()) self.after_change_password() return redirect(self.get_success_url()) From 9e8a1eaf2c8435392d4c8185f3970d33fcba3919 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Fri, 20 Dec 2019 08:10:17 -0600 Subject: [PATCH 121/239] Create .deepsource.toml --- .deepsource.toml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 00000000..b7578f0c --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,7 @@ +version = 1 + +[[analyzers]] +name = "python" +enabled = true +runtime_version = "3.x.x" + From dfe931d1753d5873fb2e3c7740061481ee337292 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 16:31:07 +0100 Subject: [PATCH 122/239] Remove Python 2 from test matrix --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index f56b6722..e9ca3d09 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,6 @@ show_missing = True [tox] envlist = checkqa - py27-dj{111}{,-pytest} py34-dj{111,20}{,-pytest} py35-dj{111,20}{,-pytest} py36-dj{111,20}{,-pytest} From 67017b9cdcc187eb395098a90d640e17762bbfb5 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 17:20:48 +0100 Subject: [PATCH 123/239] Drop redundant future statements/features `print_function` and `unicode_literals` in this case, as they are enabled in Python 3 by default. --- account/admin.py | 2 -- account/auth_backends.py | 2 -- account/conf.py | 2 -- account/context_processors.py | 2 -- account/decorators.py | 2 -- account/fields.py | 2 -- account/forms.py | 2 -- account/languages.py | 3 --- account/management/commands/expunge_deleted.py | 2 -- account/managers.py | 2 -- account/middleware.py | 2 -- account/mixins.py | 2 -- account/models.py | 2 -- account/signals.py | 2 -- account/templatetags/account_tags.py | 2 -- account/timezones.py | 2 -- account/urls.py | 2 -- account/utils.py | 2 -- account/views.py | 2 -- docs/conf.py | 2 -- 20 files changed, 41 deletions(-) diff --git a/account/admin.py b/account/admin.py index 48a0ce6a..20ae94ee 100644 --- a/account/admin.py +++ b/account/admin.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib import admin from account.models import ( diff --git a/account/auth_backends.py b/account/auth_backends.py index b4ae5863..5cc6dd62 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend from django.db.models import Q diff --git a/account/conf.py b/account/conf.py index 8d08153e..a820f988 100644 --- a/account/conf.py +++ b/account/conf.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import importlib from django.conf import settings diff --git a/account/context_processors.py b/account/context_processors.py index f10a9d8c..73dd6525 100644 --- a/account/context_processors.py +++ b/account/context_processors.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from account.conf import settings from account.models import Account diff --git a/account/decorators.py b/account/decorators.py index 13ac74c3..4f5a0147 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import functools from django.contrib.auth import REDIRECT_FIELD_NAME diff --git a/account/fields.py b/account/fields.py index 3049f928..c9b5ef3f 100644 --- a/account/fields.py +++ b/account/fields.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import models from account.conf import settings diff --git a/account/forms.py b/account/forms.py index 9c5f9b0e..843eebc4 100644 --- a/account/forms.py +++ b/account/forms.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import re from django import forms diff --git a/account/languages.py b/account/languages.py index 059fed25..6bb83ba9 100644 --- a/account/languages.py +++ b/account/languages.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.utils.translation import get_language_info diff --git a/account/management/commands/expunge_deleted.py b/account/management/commands/expunge_deleted.py index a424cc13..7313d55b 100644 --- a/account/management/commands/expunge_deleted.py +++ b/account/management/commands/expunge_deleted.py @@ -1,5 +1,3 @@ -from __future__ import print_function, unicode_literals - from django.core.management.base import BaseCommand from account.models import AccountDeletion diff --git a/account/managers.py b/account/managers.py index 3c7776b0..ec30d4d4 100644 --- a/account/managers.py +++ b/account/managers.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.db import models diff --git a/account/middleware.py b/account/middleware.py index e3216239..17c897f4 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import django from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME diff --git a/account/mixins.py b/account/mixins.py index 8292f234..ed3af24e 100644 --- a/account/mixins.py +++ b/account/mixins.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib.auth import REDIRECT_FIELD_NAME from account.compat import is_authenticated diff --git a/account/models.py b/account/models.py index 70d78cd9..2db45674 100644 --- a/account/models.py +++ b/account/models.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import datetime import operator diff --git a/account/signals.py b/account/signals.py index 40c7dbdd..a648193b 100644 --- a/account/signals.py +++ b/account/signals.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import django.dispatch user_signed_up = django.dispatch.Signal(providing_args=["user", "form"]) diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 1765bdba..c01dc451 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django import template from django.template.base import kwarg_re from django.template.defaulttags import URLNode diff --git a/account/timezones.py b/account/timezones.py index cbc1e014..265f5528 100644 --- a/account/timezones.py +++ b/account/timezones.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - TIMEZONES = [ ("Africa/Abidjan", "Africa/Abidjan"), ("Africa/Accra", "Africa/Accra"), diff --git a/account/urls.py b/account/urls.py index bbd2d17e..3a52e728 100644 --- a/account/urls.py +++ b/account/urls.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf.urls import url from account.views import ( diff --git a/account/utils.py b/account/utils.py index 5f2ae8b2..84ddff21 100644 --- a/account/utils.py +++ b/account/utils.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import datetime import functools diff --git a/account/views.py b/account/views.py index cdff0f13..e9e87e25 100644 --- a/account/views.py +++ b/account/views.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib import auth, messages from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password diff --git a/docs/conf.py b/docs/conf.py index 82cea0c9..46109091 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os import sys From 2606a6b7ce0e9a80e75a99ebd4bf1ec070b0fb9c Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 17:59:03 +0100 Subject: [PATCH 124/239] Drop compatibility handling in account.compat The `django.core.urlresolvers` module was moved to `django.urls` in version 1.10, and deprecated in version 2.0 Since the lowest version of Django we're using here is 1.11 we can safely use functions from the latter module. Also, as of Django 1.10, `user.is_authenticated` is a property, and not a method: https://docs.djangoproject.com/en/1.11/ref/contrib/auth/#django.contrib.auth.models.User.is_authenticated So we can safely drop that part of the compatibility handling, too. --- account/compat.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/account/compat.py b/account/compat.py index 94704db1..1268f7da 100644 --- a/account/compat.py +++ b/account/compat.py @@ -1,13 +1,5 @@ -import django - -try: - from django.core.urlresolvers import resolve, reverse, NoReverseMatch -except ImportError: - from django.urls import resolve, reverse, NoReverseMatch # noqa +from django.urls import resolve, reverse, NoReverseMatch # noqa def is_authenticated(user): - if django.VERSION >= (1, 10): - return user.is_authenticated - else: - return user.is_authenticated() + return user.is_authenticated From 1c5a7257721cae1bbc483a6684e793c669b1ee0e Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 18:38:23 +0100 Subject: [PATCH 125/239] Drop account.compat * Turn instances of `account.compat.NoReverseMatch`, `account.compat.resolve` and `account.compat.reverse` to their `django.urls` equivalent. * Also turn instances of `account.compat.is_authenticated(user)` to `user.is_authenticated`. --- account/compat.py | 5 ----- account/decorators.py | 3 +-- account/middleware.py | 6 +++--- account/mixins.py | 3 +-- account/models.py | 4 ++-- account/tests/test_password.py | 2 +- account/tests/test_views.py | 2 +- account/utils.py | 2 +- account/views.py | 18 +++++++++--------- 9 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 account/compat.py diff --git a/account/compat.py b/account/compat.py deleted file mode 100644 index 1268f7da..00000000 --- a/account/compat.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.urls import resolve, reverse, NoReverseMatch # noqa - - -def is_authenticated(user): - return user.is_authenticated diff --git a/account/decorators.py b/account/decorators.py index 4f5a0147..9127f84c 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -3,7 +3,6 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.decorators import available_attrs -from account.compat import is_authenticated from account.utils import handle_redirect_to_login @@ -15,7 +14,7 @@ def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url def decorator(view_func): @functools.wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): - if is_authenticated(request.user): + if request.user.is_authenticated: return view_func(request, *args, **kwargs) return handle_redirect_to_login( request, diff --git a/account/middleware.py b/account/middleware.py index 17c897f4..f669c054 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -2,12 +2,12 @@ from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect, QueryDict +from django.urls import resolve, reverse from django.utils import timezone, translation from django.utils.cache import patch_vary_headers from django.utils.translation import ugettext_lazy as _ from account import signals -from account.compat import is_authenticated, resolve, reverse from account.conf import settings from account.models import Account from account.utils import check_password_expired @@ -34,7 +34,7 @@ class LocaleMiddleware(BaseMiddleware): """ def get_language_for_user(self, request): - if is_authenticated(request.user): + if request.user.is_authenticated: try: account = Account.objects.get(user=request.user) return account.language @@ -73,7 +73,7 @@ def process_request(self, request): class ExpiredPasswordMiddleware(BaseMiddleware): def process_request(self, request): - if is_authenticated(request.user) and not request.user.is_staff: + if request.user.is_authenticated and not request.user.is_staff: next_url = resolve(request.path).url_name # Authenticated users must be allowed to access # "change password" page and "log out" page. diff --git a/account/mixins.py b/account/mixins.py index ed3af24e..0600b10a 100644 --- a/account/mixins.py +++ b/account/mixins.py @@ -1,6 +1,5 @@ from django.contrib.auth import REDIRECT_FIELD_NAME -from account.compat import is_authenticated from account.conf import settings from account.utils import handle_redirect_to_login @@ -14,7 +13,7 @@ def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs - if is_authenticated(request.user): + if request.user.is_authenticated: return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs) return self.redirect_to_login() diff --git a/account/models.py b/account/models.py index 2db45674..1ff3ebc0 100644 --- a/account/models.py +++ b/account/models.py @@ -7,13 +7,13 @@ from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver +from django.urls import reverse from django.utils import six, timezone, translation from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ import pytz from account import signals -from account.compat import is_authenticated, reverse from account.conf import settings from account.fields import TimeZoneField from account.hooks import hookset @@ -42,7 +42,7 @@ class Account(models.Model): @classmethod def for_request(cls, request): user = getattr(request, "user", None) - if user and is_authenticated(user): + if user and user.is_authenticated: try: return Account._default_manager.get(user=user) except Account.DoesNotExist: diff --git a/account/tests/test_password.py b/account/tests/test_password.py index 21367476..0b2f23b5 100644 --- a/account/tests/test_password.py +++ b/account/tests/test_password.py @@ -4,10 +4,10 @@ from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.models import User from django.test import TestCase, modify_settings, override_settings +from django.urls import reverse import pytz -from ..compat import reverse from ..models import PasswordExpiry, PasswordHistory from ..utils import check_password_expired diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 88c4fda8..d774c9d9 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -2,10 +2,10 @@ from django.contrib.auth.models import User from django.core import mail from django.test import TestCase, override_settings +from django.urls import reverse from django.utils.http import int_to_base36 from django.utils.six.moves.urllib.parse import urlparse -from account.compat import reverse from account.models import EmailConfirmation, SignupCode from account.views import INTERNAL_RESET_URL_TOKEN, PasswordResetTokenView diff --git a/account/utils.py b/account/utils.py index 84ddff21..1dbe9a8e 100644 --- a/account/utils.py +++ b/account/utils.py @@ -4,9 +4,9 @@ from django.contrib.auth import get_user_model from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict +from django.urls import NoReverseMatch, reverse import pytz -from account.compat import NoReverseMatch, reverse from account.conf import settings from .models import PasswordHistory diff --git a/account/views.py b/account/views.py index e9e87e25..48589146 100644 --- a/account/views.py +++ b/account/views.py @@ -5,6 +5,7 @@ from django.contrib.sites.shortcuts import get_current_site from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.http import base36_to_int, int_to_base36 from django.utils.translation import ugettext_lazy as _ @@ -15,7 +16,6 @@ from django.views.generic.edit import FormView from account import signals -from account.compat import is_authenticated, reverse from account.conf import settings from account.forms import ( ChangePasswordForm, @@ -172,14 +172,14 @@ def setup_signup_code(self): self.signup_code_present = False def get(self, *args, **kwargs): - if is_authenticated(self.request.user): + if self.request.user.is_authenticated: return redirect(default_redirect(self.request, settings.ACCOUNT_LOGIN_REDIRECT_URL)) if not self.is_open(): return self.closed() return super(SignupView, self).get(*args, **kwargs) def post(self, *args, **kwargs): - if is_authenticated(self.request.user): + if self.request.user.is_authenticated: raise Http404() if not self.is_open(): return self.closed() @@ -365,7 +365,7 @@ def dispatch(self, *args, **kwargs): return super(LoginView, self).dispatch(*args, **kwargs) def get(self, *args, **kwargs): - if is_authenticated(self.request.user): + if self.request.user.is_authenticated: return redirect(self.get_success_url()) return super(LoginView, self).get(*args, **kwargs) @@ -430,13 +430,13 @@ def dispatch(self, *args, **kwargs): return super(LogoutView, self).dispatch(*args, **kwargs) def get(self, *args, **kwargs): - if not is_authenticated(self.request.user): + if not self.request.user.is_authenticated: return redirect(self.get_redirect_url()) ctx = self.get_context_data() return self.render_to_response(ctx) def post(self, *args, **kwargs): - if is_authenticated(self.request.user): + if self.request.user.is_authenticated: auth.logout(self.request) return redirect(self.get_redirect_url()) @@ -534,7 +534,7 @@ def get_context_data(self, **kwargs): return ctx def get_redirect_url(self): - if is_authenticated(self.user): + if self.user.is_authenticated: if not settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL: return settings.ACCOUNT_LOGIN_REDIRECT_URL return settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL @@ -567,12 +567,12 @@ class ChangePasswordView(PasswordMixin, FormView): fallback_url_setting = "ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL" def get(self, *args, **kwargs): - if not is_authenticated(self.request.user): + if not self.request.user.is_authenticated: return redirect("account_password_reset") return super(ChangePasswordView, self).get(*args, **kwargs) def post(self, *args, **kwargs): - if not is_authenticated(self.request.user): + if not self.request.user.is_authenticated: return HttpResponseForbidden() return super(ChangePasswordView, self).post(*args, **kwargs) From 9f707c39f9dffd540e6007d5df019d5873c57135 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 19:17:21 +0100 Subject: [PATCH 126/239] Drop unicode compatibility handlers --- account/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/account/models.py b/account/models.py index 1ff3ebc0..c04463ad 100644 --- a/account/models.py +++ b/account/models.py @@ -9,7 +9,6 @@ from django.dispatch import receiver from django.urls import reverse from django.utils import six, timezone, translation -from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ import pytz @@ -27,7 +26,6 @@ from urllib import urlencode -@python_2_unicode_compatible class Account(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="account", verbose_name=_("user"), on_delete=models.CASCADE) @@ -110,7 +108,6 @@ def user_post_save(sender, **kwargs): Account.create(user=user) -@python_2_unicode_compatible class AnonymousAccount(object): def __init__(self, request=None): @@ -125,7 +122,6 @@ def __str__(self): return "AnonymousAccount" -@python_2_unicode_compatible class SignupCode(models.Model): class AlreadyExists(Exception): @@ -248,7 +244,6 @@ def save(self, **kwargs): self.signup_code.calculate_use_count() -@python_2_unicode_compatible class EmailAddress(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) @@ -299,7 +294,6 @@ def change(self, new_email, confirm=True): self.send_confirmation() -@python_2_unicode_compatible class EmailConfirmation(models.Model): email_address = models.ForeignKey(EmailAddress, on_delete=models.CASCADE) From 61fe6b623d6cf90d03e8214e714546bd752a47f8 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Tue, 21 Jan 2020 23:13:59 +0100 Subject: [PATCH 127/239] Drop module django.utils.six Carry out the following transformations: * ~.six.moves.reduce ==> functools.reduce * ~.six.StringIO ==> io.StringIO * ~.six.moves.urllib.parse.urlparse ==> urllib.parse.urlparse --- account/models.py | 5 +++-- account/tests/test_commands.py | 3 ++- account/tests/test_views.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/account/models.py b/account/models.py index c04463ad..52e5dc5e 100644 --- a/account/models.py +++ b/account/models.py @@ -1,4 +1,5 @@ import datetime +import functools import operator from django.contrib.auth.models import AnonymousUser @@ -8,7 +9,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse -from django.utils import six, timezone, translation +from django.utils import timezone, translation from django.utils.translation import ugettext_lazy as _ import pytz @@ -159,7 +160,7 @@ def exists(cls, code=None, email=None): checks.append(Q(email=code)) if not checks: return False - return cls._default_manager.filter(six.moves.reduce(operator.or_, checks)).exists() + return cls._default_manager.filter(functools.reduce(operator.or_, checks)).exists() @classmethod def create(cls, **kwargs): diff --git a/account/tests/test_commands.py b/account/tests/test_commands.py index a67ae375..8e6cc5e1 100644 --- a/account/tests/test_commands.py +++ b/account/tests/test_commands.py @@ -1,7 +1,8 @@ +from io import StringIO + from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import TestCase, override_settings -from django.utils.six import StringIO from ..conf import settings from ..models import PasswordExpiry, PasswordHistory diff --git a/account/tests/test_views.py b/account/tests/test_views.py index d774c9d9..24343e37 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -1,10 +1,11 @@ +from urllib.parse import urlparse + from django.conf import settings from django.contrib.auth.models import User from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse from django.utils.http import int_to_base36 -from django.utils.six.moves.urllib.parse import urlparse from account.models import EmailConfirmation, SignupCode from account.views import INTERNAL_RESET_URL_TOKEN, PasswordResetTokenView From f9313e07db88742f53af39ee72a0a1ac74706437 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Wed, 22 Jan 2020 00:02:10 +0100 Subject: [PATCH 128/239] Drop compatibility handlers for stdlib modules * collections.OrderedDict exists across all supported versions of Python * urlparse.urlparse ==> urllib.parse.urlparse * urlparse.urlunparse ==> urllib.parse.urlunparse * urllib.urlencode ==> urllib.parse.urlencode --- account/forms.py | 11 +++-------- account/middleware.py | 8 ++------ account/models.py | 6 +----- account/utils.py | 6 +----- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/account/forms.py b/account/forms.py index 843eebc4..38f39cfb 100644 --- a/account/forms.py +++ b/account/forms.py @@ -1,4 +1,5 @@ import re +from collections import OrderedDict from django import forms from django.contrib import auth @@ -11,12 +12,6 @@ from account.models import EmailAddress from account.utils import get_user_lookup_kwargs -try: - from collections import OrderedDict -except ImportError: - OrderedDict = None - - alnum_re = re.compile(r"^\w+$") @@ -126,7 +121,7 @@ class LoginUsernameForm(LoginForm): def __init__(self, *args, **kwargs): super(LoginUsernameForm, self).__init__(*args, **kwargs) field_order = ["username", "password", "remember"] - if not OrderedDict or hasattr(self.fields, "keyOrder"): + if hasattr(self.fields, "keyOrder"): self.fields.keyOrder = field_order else: self.fields = OrderedDict((k, self.fields[k]) for k in field_order) @@ -141,7 +136,7 @@ class LoginEmailForm(LoginForm): def __init__(self, *args, **kwargs): super(LoginEmailForm, self).__init__(*args, **kwargs) field_order = ["email", "password", "remember"] - if not OrderedDict or hasattr(self.fields, "keyOrder"): + if hasattr(self.fields, "keyOrder"): self.fields.keyOrder = field_order else: self.fields = OrderedDict((k, self.fields[k]) for k in field_order) diff --git a/account/middleware.py b/account/middleware.py index f669c054..1d62ebbc 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,3 +1,5 @@ +from urllib.parse import urlparse, urlunparse + import django from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME @@ -12,12 +14,6 @@ from account.models import Account from account.utils import check_password_expired -try: - from urllib.parse import urlparse, urlunparse -except ImportError: # python 2 - from urlparse import urlparse, urlunparse - - if django.VERSION >= (1, 10): from django.utils.deprecation import MiddlewareMixin as BaseMiddleware else: diff --git a/account/models.py b/account/models.py index 52e5dc5e..10122691 100644 --- a/account/models.py +++ b/account/models.py @@ -1,6 +1,7 @@ import datetime import functools import operator +from urllib.parse import urlencode from django.contrib.auth.models import AnonymousUser from django.contrib.sites.models import Site @@ -21,11 +22,6 @@ from account.managers import EmailAddressManager, EmailConfirmationManager from account.signals import signup_code_sent, signup_code_used -try: - from urllib.parse import urlencode -except ImportError: # python 2 - from urllib import urlencode - class Account(models.Model): diff --git a/account/utils.py b/account/utils.py index 1dbe9a8e..f34f3016 100644 --- a/account/utils.py +++ b/account/utils.py @@ -1,5 +1,6 @@ import datetime import functools +from urllib.parse import urlparse, urlunparse from django.contrib.auth import get_user_model from django.core.exceptions import SuspiciousOperation @@ -11,11 +12,6 @@ from .models import PasswordHistory -try: - from urllib.parse import urlparse, urlunparse -except ImportError: # python 2 - from urlparse import urlparse, urlunparse - def get_user_lookup_kwargs(kwargs): result = {} From 1d6f430cca0319f0d74af93850f6dc8dac57f324 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Wed, 22 Jan 2020 07:45:00 +0100 Subject: [PATCH 129/239] Prefer settings.MIDDLEWARE to settings.MIDDLEWARE_CLASSES * The MIDDLEWARE_CLASSES setting was deprecated in Django 1.10 and the MIDDLEWARE setting took precedence. * Support for the old-style MIDDLEWARE_CLASSES setting was dropped in Django 2.0 --- account/middleware.py | 7 +------ account/tests/settings.py | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index 1d62ebbc..179a9a81 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,12 +1,12 @@ from urllib.parse import urlparse, urlunparse -import django from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect, QueryDict from django.urls import resolve, reverse from django.utils import timezone, translation from django.utils.cache import patch_vary_headers +from django.utils.deprecation import MiddlewareMixin as BaseMiddleware from django.utils.translation import ugettext_lazy as _ from account import signals @@ -14,11 +14,6 @@ from account.models import Account from account.utils import check_password_expired -if django.VERSION >= (1, 10): - from django.utils.deprecation import MiddlewareMixin as BaseMiddleware -else: - BaseMiddleware = object - class LocaleMiddleware(BaseMiddleware): """ diff --git a/account/tests/settings.py b/account/tests/settings.py index 5186f65b..1fffaf44 100644 --- a/account/tests/settings.py +++ b/account/tests/settings.py @@ -45,4 +45,3 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware" ] -MIDDLEWARE_CLASSES = MIDDLEWARE From fd32825c66b49f189aedeb50d466571a9b80aad8 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Wed, 22 Jan 2020 08:02:45 +0100 Subject: [PATCH 130/239] Update setup and docs --- README.rst | 4 ++-- setup.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 2c40fd77..e93cc4a1 100644 --- a/README.rst +++ b/README.rst @@ -63,8 +63,8 @@ Features Requirements -------------- -* Django 1.8, 1.10, 1.11, or 2.0 -* Python 2.7, 3.4, 3.5, or 3.6 +* Django 1.11 or 2.0 +* Python 3.4, 3.5, or 3.6 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) diff --git a/setup.py b/setup.py index b4238c68..9b26bb49 100644 --- a/setup.py +++ b/setup.py @@ -33,8 +33,12 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', "Framework :: Django", + 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.0', ] ) From 6cfdd5198854f2a2bdf6d079bec9ec4e4ede9496 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Wed, 22 Jan 2020 11:10:59 +0100 Subject: [PATCH 131/239] Fix issues identified by deepsource * PERFORMAMCE issue in `account/models.py` 1 occurence of inheriting from `object` * ANTI-PATTERN in `account/templatetags/account_tags.py`: 1 occurrence of using `len` without comparison to determine if a sequence is empty ```python if len(bits): ... ``` Was changed to: ```python if len(bits) > 0: ... ``` * BUG RISK in `account/auth_backends.py` Match parameters in __overriding__ methods with those in the respective __overriden__ methods ``` def authenticate(self, *args, **credentials): ... ``` Was changed to ``` def authenticate(self, request, username=None, password=None, **kwargs): ... ``` Also, the body of the function was updated accordingly. * ANTI-PATTERN in `account/models.py` Drop occurrences of re-defining variables from outer scope. ``` def now(self): now = datetime.datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")) timezone = settings.TIME_ZONE if not self.timezone else self.timezone return now.astimezone(pytz.timezone(timezone)) ``` Was updated to ``` def now(self): now = datetime.datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")) tz = settings.TIME_ZONE if not self.timezone else self.timezone return now.astimezone(pytz.timezone(tz)) ``` To avoid shadowing the Python `timezone` library which was imported at the top-level * STYLE issue (2 occurrences) in `account/auth_backends.py` Fix inconsistent `return` statements -- either all return statements in a function should return an expression, or none of them should. --- account/auth_backends.py | 39 ++++++++++++++-------------- account/models.py | 10 +++---- account/templatetags/account_tags.py | 2 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index 5cc6dd62..d3777edb 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -8,35 +8,36 @@ class UsernameAuthenticationBackend(ModelBackend): - def authenticate(self, *args, **credentials): + def authenticate(self, request, username=None, password=None, **kwargs): + if username is None or password is None: + return None + User = get_user_model() try: lookup_kwargs = get_user_lookup_kwargs({ - "{username}__iexact": credentials["username"] + "{username}__iexact": username }) user = User.objects.get(**lookup_kwargs) - except (User.DoesNotExist, KeyError): + except User.DoesNotExist: return None - else: - try: - if user.check_password(credentials["password"]): - return user - except KeyError: - return None + + if user.check_password(password): + return user class EmailAuthenticationBackend(ModelBackend): - def authenticate(self, *args, **credentials): + def authenticate(self, request, username=None, password=None, **kwargs): qs = EmailAddress.objects.filter(Q(primary=True) | Q(verified=True)) + + if username is None or password is None: + return None + try: - email_address = qs.get(email__iexact=credentials["username"]) - except (EmailAddress.DoesNotExist, KeyError): + email_address = qs.get(email__iexact=username) + except EmailAddress.DoesNotExist: return None - else: - user = email_address.user - try: - if user.check_password(credentials["password"]): - return user - except KeyError: - return None + + user = email_address.user + if user.check_password(password): + return user diff --git a/account/models.py b/account/models.py index 10122691..ff002446 100644 --- a/account/models.py +++ b/account/models.py @@ -70,18 +70,18 @@ def now(self): Returns a timezone aware datetime localized to the account's timezone. """ now = datetime.datetime.utcnow().replace(tzinfo=pytz.timezone("UTC")) - timezone = settings.TIME_ZONE if not self.timezone else self.timezone - return now.astimezone(pytz.timezone(timezone)) + tz = settings.TIME_ZONE if not self.timezone else self.timezone + return now.astimezone(pytz.timezone(tz)) def localtime(self, value): """ Given a datetime object as value convert it to the timezone of the account. """ - timezone = settings.TIME_ZONE if not self.timezone else self.timezone + tz = settings.TIME_ZONE if not self.timezone else self.timezone if value.tzinfo is None: value = pytz.timezone(settings.TIME_ZONE).localize(value) - return value.astimezone(pytz.timezone(timezone)) + return value.astimezone(pytz.timezone(tz)) @receiver(post_save, sender=settings.AUTH_USER_MODEL) @@ -105,7 +105,7 @@ def user_post_save(sender, **kwargs): Account.create(user=user) -class AnonymousAccount(object): +class AnonymousAccount: def __init__(self, request=None): self.user = AnonymousUser() diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index c01dc451..a98ad35a 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -96,7 +96,7 @@ def urlnext(parser, token): asvar = bits[-1] bits = bits[:-2] - if len(bits): + if len(bits) > 0: for bit in bits: match = kwarg_re.match(bit) if not match: From 5d792e2ac761a3b19d269b62b19f8afa2d30fb1b Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Wed, 22 Jan 2020 17:39:38 -0600 Subject: [PATCH 132/239] Dropping support for django 2.0, python 2.7, 3.4, 3.5 --- .DS_Store | Bin 0 -> 6148 bytes .circleci/config.yml | 68 +++++++++++++++++++++++++------------------ setup.py | 11 +++---- tox.ini | 12 ++++---- 4 files changed, 51 insertions(+), 40 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9fa3b800eb5bbe38ccfc84fc69665dbdb9ffcfb2 GIT binary patch literal 6148 zcmeHKF=_)r43rWV4ryGb+!qA=gT**6@CO2k2?pbU!LF~$yYjTmNFv0X-5AoC5lFME z)$DOooK9xui^KceY-VOtIMKcs#>R8{$nGlRKsfGrv#qTWH^!>99|VB literal 0 HcmV?d00001 diff --git a/.circleci/config.yml b/.circleci/config.yml index 992a6751..32872461 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -34,61 +34,71 @@ jobs: lint: <<: *common docker: - - image: circleci/python:3.6.1 + - image: circleci/python:3.8 environment: - TOXENV=checkqa - py27dj111: + - TOXENV=checkqa + - UPLOAD_COVERAGE=0 + py36dj111: <<: *common docker: - - image: circleci/python:2.7 + - image: circleci/python:3.6 environment: - TOXENV=py27-dj111 - py34dj111: + TOXENV=py36-dj111 + py36dj22: <<: *common docker: - - image: circleci/python:3.4 + - image: circleci/python:3.6 environment: - TOXENV=py34-dj111 - py34dj20: + TOXENV=py36-dj22 + py36dj30: <<: *common docker: - - image: circleci/python:3.4 + - image: circleci/python:3.6 environment: - TOXENV=py34-dj20 - py35dj111: + TOXENV=py36-dj30 + py37dj111: <<: *common docker: - - image: circleci/python:3.5 + - image: circleci/python:3.7 environment: - TOXENV=py35-dj111 - py35dj20: + TOXENV=py37-dj111 + py37dj22: <<: *common docker: - - image: circleci/python:3.5 + - image: circleci/python:3.7 environment: - TOXENV=py35-dj20 - py36dj111: + TOXENV=py37-dj22 + py37dj30: <<: *common docker: - - image: circleci/python:3.6 + - image: circleci/python:3.7 environment: - TOXENV=py36-dj111 - py36dj20: + TOXENV=py37-dj30 + py38dj22: <<: *common docker: - - image: circleci/python:3.6 + - image: circleci/python:3.8 + environment: + TOXENV=py38-dj22 + py38dj30: + <<: *common + docker: + - image: circleci/python:3.8 environment: - TOXENV=py36-dj20 + TOXENV=py38-dj30 workflows: version: 2 test: jobs: - lint - - py27dj111 - - py34dj111 - - py34dj20 - - py35dj111 - - py35dj20 - py36dj111 - - py36dj20 + - py36dj21 + - py36dj22 + - py36dj30 + - py37dj111 + - py37dj21 + - py37dj22 + - py37dj30 + - py38dj22 + - py38dj30 diff --git a/setup.py b/setup.py index 9b26bb49..a7390e01 100644 --- a/setup.py +++ b/setup.py @@ -29,16 +29,17 @@ classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", + "Framework :: Django", + 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', - "Framework :: Django", - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ] ) diff --git a/tox.ini b/tox.ini index e9ca3d09..4c9f5871 100644 --- a/tox.ini +++ b/tox.ini @@ -28,17 +28,17 @@ show_missing = True [tox] envlist = checkqa - py34-dj{111,20}{,-pytest} - py35-dj{111,20}{,-pytest} - py36-dj{111,20}{,-pytest} + py{36,37}-dj{111,22,30}{,-pytest} + py38-dj{22,30}{,-pytest} [testenv] passenv = CI CIRCLECI CIRCLE_* deps = - coverage + coverage<5 codecov - dj111: Django>=1.11,<2.0 - dj20: Django<2.1 + dj111: Django>=1.11,<1.12 + dj22: Django>=2.2,<3.0 + dj30: Django>=3.0,<3.1 master: https://github.com/django/django/tarball/master extras = pytest: pytest From e0e47d926686ee21018d256f6b906f5d4fbd8fc5 Mon Sep 17 00:00:00 2001 From: KatherineMichel Date: Mon, 17 Feb 2020 17:48:36 -0600 Subject: [PATCH 133/239] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 9fa3b800eb5bbe38ccfc84fc69665dbdb9ffcfb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKF=_)r43rWV4ryGb+!qA=gT**6@CO2k2?pbU!LF~$yYjTmNFv0X-5AoC5lFME z)$DOooK9xui^KceY-VOtIMKcs#>R8{$nGlRKsfGrv#qTWH^!>99|VB From 5f5fecc6f64dd48b7cda90d7cffa0e53ca6ea2ab Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 18:12:44 -0600 Subject: [PATCH 134/239] Improving .gitignore --- .gitignore | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 0d1d86dd..3ea1898a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,44 @@ -*.pyc -.coverage -*.egg-info +MANIFEST +.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# Distribution / packaging +.Python +env/ build/ +develop-eggs/ dist/ -.eggs -.tox +docs/_build/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs +.python-version + +# Pipfile +Pipfile +Pipfile.lock + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# IDEs +.idea/ From d27e5c3ca4f781e9a28420d1b532b37d338d273d Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 18:19:57 -0600 Subject: [PATCH 135/239] Updating license --- LICENSE | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/LICENSE b/LICENSE index 82ec3974..4fbc79df 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,21 @@ -# Copyright (c) 2012-2014 James Tauber and contributors -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +The MIT License (MIT) + +Copyright (c) 2012-2020 James Tauber and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From 3bcffb98a22056d78d7fe3c0feac2dbff8aadbf8 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 18:41:32 -0600 Subject: [PATCH 136/239] Removing CONTRIBUTING.md in favor of global community health file --- CONTRIBUTING.md | 170 ------------------------------------------------ MANIFEST.in | 1 - 2 files changed, 171 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 4e1c01db..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,170 +0,0 @@ -# How to Contribute - -There are many ways you can help contribute to django-user-accounts. - -Contributing code, writing documentation, reporting bugs, as well as -reading and providing feedback on issues and pull requests, all are -valid and necessary ways to help. - -## Committing Code - -The great thing about using a distributed versioning control system like git -is that everyone becomes a committer. When other people write good patches -it makes it very easy to include their fixes/features and give them proper -credit for the work. - -We recommend that you do all your work in a separate branch. When you -are ready to work on a bug or a new feature create yourself a new branch. The -reason why this is important is you can commit as often you like. When you are -ready you can merge in the change. Let's take a look at a common workflow: - - git checkout -b task-566 - ... fix and git commit often ... - git push origin task-566 - -The reason we have created two new branches is to stay off of `master`. -Keeping master clean of only upstream changes makes yours and ours lives -easier. You can then send us a pull request for the fix/feature. Then we can -easily review it and merge it when ready. - - -### Writing Commit Messages - -Writing a good commit message makes it simple for us to identify what your -commit does from a high-level. There are some basic guidelines we'd like to -ask you to follow. - -A critical part is that you keep the **first** line as short and sweet -as possible. This line is important because when git shows commits and it has -limited space or a different formatting option is used the first line becomes -all someone might see. If your change isn't something non-trivial or there -reasoning behind the change is not obvious, then please write up an extended -message explaining the fix, your rationale, and anything else relevant for -someone else that might be reviewing the change. Lastly, if there is a -corresponding issue in Github issues for it, use the final line to provide -a message that will link the commit message to the issue and auto-close it -if appropriate. - - Add ability to travel back in time - - You need to be driving 88 miles per hour to generate 1.21 gigawatts of - power to properly use this feature. - - Fixes #88 - - -## Coding style - -When writing code to be included in django-user-accounts keep our style in mind: - -* Follow [PEP8](http://www.python.org/dev/peps/pep-0008/) there are some - cases where we do not follow PEP8. It is an excellent starting point. -* Follow [Django's coding style](http://docs.djangoproject.com/en/dev/internals/contributing/#coding-style) - we're pretty much in agreement on Django style outlined there. - -We would like to enforce a few more strict guides not outlined by PEP8 or -Django's coding style: - -* PEP8 tries to keep line length at 80 characters. We follow it when we can, - but not when it makes a line harder to read. It is okay to go a little bit - over 80 characters if not breaking the line improves readability. -* Use double quotes not single quotes. Single quotes are allowed in cases - where a double quote is needed in the string. This makes the code read - cleaner in those cases. -* Docstrings always use three double quotes on a line of their own, so, for - example, a single line docstring should take up three lines not one. -* Imports are grouped specifically and ordered alphabetically. This is shown - in the example below. -* Always use `reverse` and never `@models.permalink`. -* Tuples should be reserved for positional data structures and not used - where a list is more appropriate. -* URL patterns should use the `url()` function rather than a tuple. - -Here is an example of these rules applied: - - # first set of imports are stdlib imports - # non-from imports go first then from style import in their own group - import csv - - # second set of imports are Django imports - from django.contrib.auth.models import User - from django.db import models - from django.urls import reverse - from django.utils import timezone - from django.utils.translation import ugettext_lazy as _ - - # third set of imports are external apps (if applicable) - from tagging.fields import TagField - - # fourth set of imports are local apps - from .fields import MarkupField - - - class Task(models.Model): - """ - A model for storing a task. - """ - - creator = models.ForeignKey(User) - created = models.DateTimeField(default=timezone.now) - modified = models.DateTimeField(default=timezone.now) - - objects = models.Manager() - - class Meta: - verbose_name = _("task") - verbose_name_plural = _("tasks") - - def __unicode__(self): - return self.summary - - def save(self, **kwargs): - self.modified = datetime.now() - super(Task, self).save(**kwargs) - - def get_absolute_url(self): - return reverse("task_detail", kwargs={"task_id": self.pk}) - - # custom methods - - - class TaskComment(models.Model): - # ... you get the point ... - pass - - -## Pull Requests - -Please keep your pull requests focused on one specific thing only. If you -have a number of contributions to make, then please send seperate pull -requests. It is much easier on maintainers to receive small, well defined, -pull requests, than it is to have a single large one that batches up a -lot of unrelated commits. - -If you ended up making multiple commits for one logical change, please -rebase into a single commit. - - git rebase -i HEAD~10 # where 10 is the number of commits back you need - -This will pop up an editor with your commits and some instructions you want -to squash commits down by replacing 'pick' with 's' to have it combined with -the commit before it. You can squash multiple ones at the same time. - -When you save and exit the text editor where you were squashing commits, git -will squash them down and then present you with another editor with commit -messages. Choose the one to apply to the squashed commit (or write a new -one entirely.) Save and exit will complete the rebase. Use a forced push to -your fork. - - git push -f - - -## Translations - -We use [Transifex](https://www.transifex.com/) to handle translations. We -discourage pull requests with changes to translations. Transifex handles -translations better than dealing them through the pull request system. - -Head over to [django-user-accounts on Transifex](https://www.transifex.com/projects/p/django-user-accounts/) -and find the language you would like to contribute. If you do not find your -language then please submit an issue and we will get it setup. diff --git a/MANIFEST.in b/MANIFEST.in index 96465f3a..f0218c17 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include .coveragerc include CHANGELOG.md -include CONTRIBUTING.md include LICENSE include tox.ini include README.rst From ff1bfc40fa1768aa4e1ec2684a4ea1d06e52e3d8 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 19:21:59 -0600 Subject: [PATCH 137/239] Updating license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4fbc79df..c9d23959 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2012-2020 James Tauber and contributors +Copyright (c) 2012-present James Tauber and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From da1b16c415959d6d47161e84e97c963d024ac867 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 20:52:56 -0600 Subject: [PATCH 138/239] Converting README.md to markdown --- MANIFEST.in | 2 +- README.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 102 ------------------------------------------ setup.py | 2 +- 4 files changed, 126 insertions(+), 104 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/MANIFEST.in b/MANIFEST.in index f0218c17..fe6c33ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,7 @@ include .coveragerc include CHANGELOG.md include LICENSE include tox.ini -include README.rst +include README.md include runtests.py recursive-include account *.html recursive-include account *.txt diff --git a/README.md b/README.md new file mode 100644 index 00000000..9e615eee --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +![](http://pinaxproject.com/pinax-design/patches/django-user-accounts.svg) + +# Django User Accounts + +[![](https://img.shields.io/pypi/v/django-user-accounts.svg)](https://pypi.python.org/pypi/django-user-accounts/) + +[![CircleCi](https://img.shields.io/circleci/project/github/pinax/django-user-accounts.svg)](https://circleci.com/gh/pinax/django-user-accounts) +[![Codecov](https://img.shields.io/codecov/c/github/pinax/django-user-accounts.svg)](https://codecov.io/gh/pinax/django-user-accounts) +[![](https://img.shields.io/github/contributors/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/graphs/contributors) +[![](https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/pulls) +[![](https://img.shields.io/github/issues-pr-closed/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/pulls?q=is%3Apr+is%3Aclosed) + +[![](http://slack.pinaxproject.com/badge.svg)](http://slack.pinaxproject.com/) +[![](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) + + +## Table of Contents + +* [About Pinax](#about-pinax) +* [Overview](#overview) + * [Features](#features) + * [Supported Django and Python versions](#supported-django-and-python-versions) +* [Requirements](#requirements) +* [Documentation](#documentation) + * [Templates](#templates) +* [Change Log](#change-log) +* [Contribute](#contribute) +* [Code of Conduct](#code-of-conduct) +* [Connect with Pinax](#connect-with-pinax) +* [License](#license) + + +## About Pinax + +Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates. This collection can be found at http://pinaxproject.com. + + +## django-user-accounts + +### Overview + +`django-user-accounts` provides a Django project with a very extensible infrastructure for dealing with user accounts. + +#### Features + +* Functionality for: + * Log in (email or username authentication) + * Sign up + * Email confirmation + * Signup tokens for private betas + * Password reset + * Password expiration + * Account management (update account settings and change password) + * Account deletion +* Extensible class-based views and hooksets +* Custom `User` model support + +#### Supported Django and Python versions + +Django / Python | 3.6 | 3.7 | 3.8 +--------------- | --- | --- | --- +1.11 | * | * | +2.2 | * | * | * +3.0 | * | * | * + + +## Requirements + +* Django 1.11, 2.2, or 3.0 +* Python 3.6, 3.7, or 3.8 +* django-appconf (included in ``install_requires``) +* pytz (included in ``install_requires``) + + +## Documentation + +See http://django-user-accounts.readthedocs.org/ for the `django-user-accounts` documentation. +On September 17th, 2015, we did a Pinax Hangout on `django-user-accounts`. You can read the recap blog post and find the video here http://blog.pinaxproject.com/2015/10/12/recap-september-pinax-hangout/. + +The Pinax documentation is available at http://pinaxproject.com/pinax/. If you would like to help us improve our documentation or write more documentation, please join our Slack team and let us know! + + +### Templates + +Default templates are provided by the `pinax-templates` app in the +[account](https://github.com/pinax/pinax-templates/tree/master/pinax/templates/templates/account) section of that project. + +Reference pinax-templates +[installation instructions](https://github.com/pinax/pinax-templates/blob/master/README.md#installation) to include these templates in your project. + +View live `pinax-templates` examples and source at [Pinax Templates](https://templates.pinaxproject.com/)! + +See the `django-user-accounts` [templates](https://django-user-accounts.readthedocs.io/en/latest/templates.html) documentation for more information. + + +## Change Log + + +## Contribute + +For an overview on how contributing to Pinax works read this [blog post](http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/) +and watch the included video, or read our [How to Contribute](http://pinaxproject.com/pinax/how_to_contribute/) section. For concrete contribution ideas, please see our +[Ways to Contribute/What We Need Help With](http://pinaxproject.com/pinax/ways_to_contribute/) section. + +In case of any questions we recommend you join our [Pinax Slack team](http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. + +We also highly recommend reading our blog post on [Open Source and Self-Care](http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). + + +## Code of Conduct + +In order to foster a kind, inclusive, and harassment-free community, the Pinax Project +has a [code of conduct](http://pinaxproject.com/pinax/code_of_conduct/). +We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. + + +## Connect with Pinax + +For updates and news regarding the Pinax Project, please follow us on Twitter [@pinaxproject](https://twitter.com/pinaxproject) and check out our [Pinax Project blog](http://blog.pinaxproject.com). + + +## License + +Copyright (c) 2012-present James Tauber and contributors under the [MIT license](https://opensource.org/licenses/MIT). \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index e93cc4a1..00000000 --- a/README.rst +++ /dev/null @@ -1,102 +0,0 @@ -.. image:: http://pinaxproject.com/pinax-design/patches/django-user-accounts.svg - -==================== -Django User Accounts -==================== - -.. image:: http://slack.pinaxproject.com/badge.svg - :target: http://slack.pinaxproject.com/ - -.. image:: https://circleci.com/gh/pinax/django-user-accounts.svg?style=svg - :target: https://circleci.com/gh/pinax/django-user-accounts - -.. image:: https://img.shields.io/codecov/c/github/pinax/django-user-accounts.svg - :target: https://codecov.io/gh/pinax/django-user-accounts - -.. image:: https://img.shields.io/pypi/v/django-user-accounts.svg - :target: https://pypi.python.org/pypi/django-user-accounts/ - -.. image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://pypi.python.org/pypi/django-user-accounts/ - -.. image:: https://img.shields.io/github/contributors/pinax/django-user-accounts.svg - :target: https://github.com/pinax/django-user-accounts/issues/ -.. image:: https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg - :target: https://github.com/pinax/django-user-accounts/issues/ -.. image:: https://img.shields.io/github/issues-pr-closed/pinax/django-user-accounts.svg - :target: https://github.com/pinax/django-user-accounts/issues/ - -Pinax -------- - -Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates. -This collection can be found at http://pinaxproject.com. - -This app was developed as part of the Pinax ecosystem but is just a Django app and can be used independently of other Pinax apps. - - -django-user-accounts -------------------------- - -``django-user-accounts`` provides a Django project with a very extensible infrastructure for dealing -with user accounts. - - -Features ----------- - -* Functionality for: - - - Log in (email or username authentication) - - Sign up - - Email confirmation - - Signup tokens for private betas - - Password reset - - Password expiration - - Account management (update account settings and change password) - - Account deletion - -* Extensible class-based views and hooksets -* Custom ``User`` model support - - -Requirements --------------- - -* Django 1.11 or 2.0 -* Python 3.4, 3.5, or 3.6 -* django-appconf (included in ``install_requires``) -* pytz (included in ``install_requires``) - - -Documentation ----------------- - -See http://django-user-accounts.readthedocs.org/ for the ``django-user-accounts`` documentation. -On September 17th, 2015, we did a Pinax Hangout on ``django-user-accounts``. You can read the recap blog post and find the video here http://blog.pinaxproject.com/2015/10/12/recap-september-pinax-hangout/. - -The Pinax documentation is available at http://pinaxproject.com/pinax/. If you would like to help us improve our documentation or write more documentation, please join our Slack team and let us know! - - -Contribute ----------------- - -See this blog post http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/ including a video, or our How to Contribute (http://pinaxproject.com/pinax/how_to_contribute/) section for an overview on how contributing to Pinax works. For concrete contribution ideas, please see our Ways to Contribute/What We Need Help With (http://pinaxproject.com/pinax/ways_to_contribute/) section. - -In case of any questions we recommend you join our Pinax Slack team (http://slack.pinaxproject.com) and ping us there instead of creating an issue on GitHub. Creating issues on GitHub is of course also valid but we are usually able to help you faster if you ping us in Slack. - -We also highly recommend reading our Open Source and Self-Care blog post (http://blog.pinaxproject.com/2016/01/19/open-source-and-self-care/). - - -Code of Conduct ------------------ - -In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/. -We ask you to treat everyone as a smart human programmer that shares an interest in Python, Django, and Pinax with you. - - - -Pinax Project Blog and Twitter --------------------------------- - -For updates and news regarding the Pinax Project, please follow us on Twitter at @pinaxproject and check out our blog http://blog.pinaxproject.com. diff --git a/setup.py b/setup.py index a7390e01..1fcc5864 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", - long_description=open("README.rst").read(), + long_description=open("README.md").read(), license="MIT", url="http://github.com/pinax/django-user-accounts", packages=find_packages(), From 04f6a5ba1da4225399ba0e3a731fe4aa42c7e1a9 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 20:55:41 -0600 Subject: [PATCH 139/239] Fix ToC templates link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e615eee..b2c355c1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ * [Supported Django and Python versions](#supported-django-and-python-versions) * [Requirements](#requirements) * [Documentation](#documentation) - * [Templates](#templates) +* [Templates](#templates) * [Change Log](#change-log) * [Contribute](#contribute) * [Code of Conduct](#code-of-conduct) From d305b66bf9498c45935bc6cd78191b352e2cfc24 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 21:12:36 -0600 Subject: [PATCH 140/239] Removing the Change Log entry from ToC --- CHANGELOG.md | 2 +- README.md | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cf1114..bd698125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# ChangeLog +# Change Log BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. diff --git a/README.md b/README.md index b2c355c1..b9298711 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ * [Requirements](#requirements) * [Documentation](#documentation) * [Templates](#templates) -* [Change Log](#change-log) * [Contribute](#contribute) * [Code of Conduct](#code-of-conduct) * [Connect with Pinax](#connect-with-pinax) @@ -93,9 +92,6 @@ View live `pinax-templates` examples and source at [Pinax Templates](https://tem See the `django-user-accounts` [templates](https://django-user-accounts.readthedocs.io/en/latest/templates.html) documentation for more information. -## Change Log - - ## Contribute For an overview on how contributing to Pinax works read this [blog post](http://blog.pinaxproject.com/2016/02/26/recap-february-pinax-hangout/) From 39178d8ef6f85ed156aa43cd7d5bf1187f6ce2a4 Mon Sep 17 00:00:00 2001 From: Katherine Michel Date: Mon, 17 Feb 2020 21:17:11 -0600 Subject: [PATCH 141/239] Removing universal wheel entry bc/Python 2 was dropped --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 62c9f325..5037aeb7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [tool:pytest] testpaths = account/tests DJANGO_SETTINGS_MODULE = account.tests.settings From 8f4c493e91a706d58b8fd0040e583c3861f52ecc Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 08:52:15 -0600 Subject: [PATCH 142/239] Should already be installed --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 32872461..b3829654 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,6 @@ common: &common command: | if [[ "$TOXENV" != checkqa ]]; then PATH=$HOME/.local/bin:$PATH - pip install --user codecov ~/.local/bin/codecov --required --flags $CIRCLE_JOB fi - save_cache: From 10ecee2754b5aea24a8fb9f5bed6ddbdc5f3f89f Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 09:01:25 -0600 Subject: [PATCH 143/239] Fix circle ci config and upgrade to 2.1 --- .circleci/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b3829654..86741fab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2.0 +version: 2.1 common: &common working_directory: ~/repo @@ -92,11 +92,9 @@ workflows: jobs: - lint - py36dj111 - - py36dj21 - py36dj22 - py36dj30 - py37dj111 - - py37dj21 - py37dj22 - py37dj30 - py38dj22 From 3cf754eb6b238695277ab6f759f7bcfc334916a2 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 09:07:48 -0600 Subject: [PATCH 144/239] Install code coverage bits along with tox --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 86741fab..0292560b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ common: &common - v2-deps- - run: name: install dependencies - command: pip install --user tox + command: pip install --user tox codecov coverage<5 - run: name: run tox command: ~/.local/bin/tox From 8811978499672c1a63adf1e7163ecb582b0c8e98 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 09:08:55 -0600 Subject: [PATCH 145/239] Install code coverage bits along with tox --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0292560b..71babfd2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,10 @@ common: &common - v2-deps- - run: name: install dependencies - command: pip install --user tox codecov coverage<5 + command: + - pip install --user tox + - pip install --user codecov + - pip install --user coverage<5 - run: name: run tox command: ~/.local/bin/tox From 27e6ad532174da3532c0ca60b7b6abbc515a9f6e Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 09:13:01 -0600 Subject: [PATCH 146/239] Install code coverage bits along with tox --- .circleci/config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 71babfd2..566b0735 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,10 +10,7 @@ common: &common - v2-deps- - run: name: install dependencies - command: - - pip install --user tox - - pip install --user codecov - - pip install --user coverage<5 + command: pip install --user tox codecov "coverage<5" - run: name: run tox command: ~/.local/bin/tox From b95fff1393e9a7806fe4d0d05b8348bb32db5c25 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 09:38:02 -0600 Subject: [PATCH 147/239] Fix up lints --- account/__init__.py | 1 - account/conf.py | 6 ++---- account/management/commands/user_password_expiry.py | 4 ++-- account/tests/test_commands.py | 6 +++--- tox.ini | 8 ++++---- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/account/__init__.py b/account/__init__.py index c6f1afd1..7d51e983 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1,4 +1,3 @@ import pkg_resources - __version__ = pkg_resources.get_distribution("django-user-accounts").version diff --git a/account/conf.py b/account/conf.py index a820f988..f36e90db 100644 --- a/account/conf.py +++ b/account/conf.py @@ -1,10 +1,8 @@ import importlib -from django.conf import settings +from django.conf import settings # noqa from django.core.exceptions import ImproperlyConfigured -from django.utils.translation import get_language_info -import pytz from account.languages import LANGUAGES from account.timezones import TIMEZONES from appconf import AppConf @@ -38,7 +36,7 @@ class AccountAppConf(AppConf): PASSWORD_USE_HISTORY = False PASSWORD_STRIP = True REMEMBER_ME_EXPIRY = 60 * 60 * 24 * 365 * 10 - USER_DISPLAY = lambda user: user.username # flake8: noqa + USER_DISPLAY = lambda user: user.username # noqa CREATE_ON_SAVE = True EMAIL_UNIQUE = True EMAIL_CONFIRMATION_REQUIRED = False diff --git a/account/management/commands/user_password_expiry.py b/account/management/commands/user_password_expiry.py index b13424c8..6733a598 100644 --- a/account/management/commands/user_password_expiry.py +++ b/account/management/commands/user_password_expiry.py @@ -25,7 +25,7 @@ def handle_label(self, username, **options): try: user = User.objects.get(username=username) except User.DoesNotExist: - return "User \"{}\" not found".format(username) + return 'User "{}" not found'.format(username) expire = options["expire"] @@ -36,4 +36,4 @@ def handle_label(self, username, **options): user.password_expiry.expiry = expire user.password_expiry.save() - return "User \"{}\" password expiration set to {} seconds".format(username, expire) + return 'User "{}" password expiration set to {} seconds'.format(username, expire) diff --git a/account/tests/test_commands.py b/account/tests/test_commands.py index 8e6cc5e1..4a734dd5 100644 --- a/account/tests/test_commands.py +++ b/account/tests/test_commands.py @@ -34,7 +34,7 @@ def test_set_explicit_password_expiry(self): user = self.UserModel.objects.get(username="patrick") user_expiry = user.password_expiry self.assertEqual(user_expiry.expiry, expiration_period) - self.assertIn("User \"{}\" password expiration set to {} seconds".format(self.user.username, expiration_period), out.getvalue()) + self.assertIn('User "{}" password expiration set to {} seconds'.format(self.user.username, expiration_period), out.getvalue()) def test_set_default_password_expiry(self): """ @@ -52,7 +52,7 @@ def test_set_default_password_expiry(self): user_expiry = user.password_expiry default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY self.assertEqual(user_expiry.expiry, default_expiration) - self.assertIn("User \"{}\" password expiration set to {} seconds".format(self.user.username, default_expiration), out.getvalue()) + self.assertIn('User "{}" password expiration set to {} seconds'.format(self.user.username, default_expiration), out.getvalue()) def test_reset_existing_password_expiry(self): """ @@ -85,7 +85,7 @@ def test_bad_username(self): bad_username, stdout=out ) - self.assertIn("User \"{}\" not found".format(bad_username), out.getvalue()) + self.assertIn('User "{}" not found'.format(bad_username), out.getvalue()) class UserPasswordHistoryTests(TestCase): diff --git a/tox.ini b/tox.ini index 4c9f5871..991c734e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ multi_line_output=3 known_django=django known_third_party=account,six,mock,appconf,jsonfield,pytz sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -skip_glob=*/account/migrations/* +skip_glob=migrations/* include_trailing_comma=True [coverage:run] @@ -55,6 +55,6 @@ commands = flake8 account isort --recursive --check-only --diff account -sp tox.ini deps = - flake8 == 3.4.1 - flake8-quotes == 0.11.0 - isort == 4.2.15 + flake8 == 3.7.9 + flake8-quotes == 2.1.1 + isort == 4.3.21 From 23f41a14a571a523881af2c737247da24327906e Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 13:27:08 -0600 Subject: [PATCH 148/239] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9298711..7b7f652f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](http://pinaxproject.com/pinax-design/patches/django-user-accounts.svg) +![](https://pinaxproject.com/pinax-design/social-banners/DUA.png) # Django User Accounts @@ -117,4 +117,4 @@ For updates and news regarding the Pinax Project, please follow us on Twitter [@ ## License -Copyright (c) 2012-present James Tauber and contributors under the [MIT license](https://opensource.org/licenses/MIT). \ No newline at end of file +Copyright (c) 2012-present James Tauber and contributors under the [MIT license](https://opensource.org/licenses/MIT). From a8902a323496fc655312d09bc834a774f82493cc Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 13:27:36 -0600 Subject: [PATCH 149/239] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7b7f652f..1462b29c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ ![](https://pinaxproject.com/pinax-design/social-banners/DUA.png) -# Django User Accounts - [![](https://img.shields.io/pypi/v/django-user-accounts.svg)](https://pypi.python.org/pypi/django-user-accounts/) [![CircleCi](https://img.shields.io/circleci/project/github/pinax/django-user-accounts.svg)](https://circleci.com/gh/pinax/django-user-accounts) @@ -14,7 +12,7 @@ [![](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -## Table of Contents +# Table of Contents * [About Pinax](#about-pinax) * [Overview](#overview) From a6a4da9952d470f23a5a4c88c232797e7592ac86 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Thu, 20 Feb 2020 16:21:45 -0600 Subject: [PATCH 150/239] 3.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1fcc5864..93ca652f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-user-accounts", - version="2.1.0", + version="3.0.0", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From 404c37bac59a8d1818e11c58d62ffa0d93880d84 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 28 Feb 2020 15:17:22 +1100 Subject: [PATCH 151/239] Update decorators for Django 3 --- account/decorators.py | 3 +-- account/tests/test_decorators.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 account/tests/test_decorators.py diff --git a/account/decorators.py b/account/decorators.py index 9127f84c..82a8658b 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -1,7 +1,6 @@ import functools from django.contrib.auth import REDIRECT_FIELD_NAME -from django.utils.decorators import available_attrs from account.utils import handle_redirect_to_login @@ -12,7 +11,7 @@ def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url to the log in page if necessary. """ def decorator(view_func): - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func) def _wrapped_view(request, *args, **kwargs): if request.user.is_authenticated: return view_func(request, *args, **kwargs) diff --git a/account/tests/test_decorators.py b/account/tests/test_decorators.py new file mode 100644 index 00000000..213d4296 --- /dev/null +++ b/account/tests/test_decorators.py @@ -0,0 +1,26 @@ +from unittest import mock + +from django.http import HttpResponse +from django.test import TestCase + +from account.decorators import login_required + + +@login_required +def mock_view(request, *args, **kwargs): + return HttpResponse('OK', status=200) + + +class LoginRequiredDecoratorTestCase(TestCase): + + def test_authenticated_user_is_allowed(self): + request = mock.MagicMock() + request.user.is_authenticated = True + response = mock_view(request) + self.assertEqual(response.status_code, 200) + + def test_unauthenticated_user_gets_redirected(self): + request = mock.MagicMock() + request.user.is_authenticated = False + response = mock_view(request) + self.assertEqual(response.status_code, 302) From 7453eb524f6dc0cbd16e7757b8901ef412282d59 Mon Sep 17 00:00:00 2001 From: Ryan Nowakowski Date: Fri, 28 Feb 2020 13:50:11 -0600 Subject: [PATCH 152/239] avoid passing a lazy string to urlparse urlparse does not support reverse_lazy as url arg. Much like Django bug [#18776](https://code.djangoproject.com/ticket/18776), urlparse will fail with `AttributeError: '__proxy__' object has no attribute 'decode'` if the url arg is from reverse_lazy. --- account/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/account/utils.py b/account/utils.py index f34f3016..4163170b 100644 --- a/account/utils.py +++ b/account/utils.py @@ -6,6 +6,7 @@ from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict from django.urls import NoReverseMatch, reverse +from django.utils.encoding import force_str import pytz from account.conf import settings @@ -90,7 +91,7 @@ def handle_redirect_to_login(request, **kwargs): raise if "/" not in login_url and "." not in login_url: raise - url_bits = list(urlparse(login_url)) + url_bits = list(urlparse(force_str(login_url))) if redirect_field_name: querystring = QueryDict(url_bits[4], mutable=True) querystring[redirect_field_name] = next_url From 6f65baa1571ef291fda2f5a8e89bd49cd67d2c1c Mon Sep 17 00:00:00 2001 From: auahmetunal Date: Thu, 2 Apr 2020 17:55:33 +0300 Subject: [PATCH 153/239] Update views.py emails are sent to django.contrib.auth User model adress instead of account EmailAddress model adress --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index f87f2cc1..e0a62e44 100644 --- a/account/views.py +++ b/account/views.py @@ -648,7 +648,7 @@ def send_email(self, email): "current_site": current_site, "password_reset_url": password_reset_url, } - hookset.send_password_reset_email([user.email], ctx) + hookset.send_password_reset_email([email], ctx) def make_token(self, user): return self.token_generator.make_token(user) From 3157151d835377a4ddf80d5514ea1edc0a2a8203 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 28 Apr 2020 21:37:23 -0500 Subject: [PATCH 154/239] Fix a 3.0 compat issue --- account/decorators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index 9127f84c..a4fa4a4e 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -1,7 +1,6 @@ import functools from django.contrib.auth import REDIRECT_FIELD_NAME -from django.utils.decorators import available_attrs from account.utils import handle_redirect_to_login @@ -12,7 +11,7 @@ def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url to the log in page if necessary. """ def decorator(view_func): - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func, assigned=functools.WRAPPER_ASSIGNMENTS) def _wrapped_view(request, *args, **kwargs): if request.user.is_authenticated: return view_func(request, *args, **kwargs) From 68458aa50a2fca5053d2c5e4a9b0dd1d06f4c007 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 28 Apr 2020 21:38:05 -0500 Subject: [PATCH 155/239] Bump to 3.0.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 93ca652f..ae743b18 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-user-accounts", - version="3.0.0", + version="3.0.1", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From ba4f1703cfecf541363dce61b930748d0a9f4db8 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 28 Apr 2020 22:02:15 -0500 Subject: [PATCH 156/239] Fix deprecations --- account/forms.py | 2 +- account/middleware.py | 2 +- account/models.py | 2 +- account/views.py | 2 +- setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/account/forms.py b/account/forms.py index 38f39cfb..ff2f0e4e 100644 --- a/account/forms.py +++ b/account/forms.py @@ -5,7 +5,7 @@ from django.contrib import auth from django.contrib.auth import get_user_model from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from account.conf import settings from account.hooks import hookset diff --git a/account/middleware.py b/account/middleware.py index 179a9a81..1e70d2b0 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -7,7 +7,7 @@ from django.utils import timezone, translation from django.utils.cache import patch_vary_headers from django.utils.deprecation import MiddlewareMixin as BaseMiddleware -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from account import signals from account.conf import settings diff --git a/account/models.py b/account/models.py index ff002446..7d479a11 100644 --- a/account/models.py +++ b/account/models.py @@ -11,7 +11,7 @@ from django.dispatch import receiver from django.urls import reverse from django.utils import timezone, translation -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import pytz from account import signals diff --git a/account/views.py b/account/views.py index f87f2cc1..291d0d36 100644 --- a/account/views.py +++ b/account/views.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.http import base36_to_int, int_to_base36 -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters diff --git a/setup.py b/setup.py index ae743b18..44910262 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-user-accounts", - version="3.0.1", + version="3.0.2", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From c9d3d3e021aa3d4aa4ebedc6d335f2dbd74c3189 Mon Sep 17 00:00:00 2001 From: Mfon Eti-mfon Date: Fri, 27 Mar 2020 12:21:40 +0100 Subject: [PATCH 157/239] Drop deprecated force_text() https://docs.djangoproject.com/en/dev/releases/3.0/#django-utils-encoding-force-text-and-smart-text --- account/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account/forms.py b/account/forms.py index ff2f0e4e..b093df75 100644 --- a/account/forms.py +++ b/account/forms.py @@ -4,7 +4,7 @@ from django import forms from django.contrib import auth from django.contrib.auth import get_user_model -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ from account.conf import settings @@ -25,7 +25,7 @@ def __init__(self, *args, **kwargs): def to_python(self, value): if value in self.empty_values: return "" - value = force_text(value) + value = force_str(value) if self.strip: value = value.strip() return value From 469938168b330d5c059fad5bb90cb34df75a30a2 Mon Sep 17 00:00:00 2001 From: KatherineMichel Date: Wed, 19 Aug 2020 17:41:46 -0500 Subject: [PATCH 158/239] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 44910262..923e05b7 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="django-user-accounts", - version="3.0.2", + version="3.0.3", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From b81b877d5289e990c872bfc66e35901b31372ae8 Mon Sep 17 00:00:00 2001 From: KatherineMichel Date: Wed, 19 Aug 2020 18:08:36 -0500 Subject: [PATCH 159/239] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd698125..49a07fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 3.0.3 + +* Drop deprecated `force_text()` + +## 3.0.2 + +* Drop Django 2.0 and Python 2,7, 3.4, and 3.5 support +* Add Django 2.1, 2.2 and 3.0, and Python 3.7 and 3.8 support +* Update packaging configs + ## 2.0.3 * fixed breaking change in 2.0.2 where context did not have uidb36 and token From ed26a1f9e66d4e4013261b43fb99d6c366c14723 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Mon, 21 Dec 2020 22:57:45 -0600 Subject: [PATCH 160/239] Update CI to use Github Actions --- .circleci/config.yml | 101 -------------------------------------- .github/workflows/ci.yaml | 68 +++++++++++++++++++++++++ pyproject.toml | 9 ++++ setup.py | 10 ++-- tox.ini | 44 +---------------- 5 files changed, 84 insertions(+), 148 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 pyproject.toml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 566b0735..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,101 +0,0 @@ -version: 2.1 - -common: &common - working_directory: ~/repo - steps: - - checkout - - restore_cache: - keys: - - v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} - - v2-deps- - - run: - name: install dependencies - command: pip install --user tox codecov "coverage<5" - - run: - name: run tox - command: ~/.local/bin/tox - - run: - name: upload coverage report - command: | - if [[ "$TOXENV" != checkqa ]]; then - PATH=$HOME/.local/bin:$PATH - ~/.local/bin/codecov --required --flags $CIRCLE_JOB - fi - - save_cache: - paths: - - .tox - - ~/.cache/pip - - ~/.local - - ./eggs - key: v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} - -jobs: - lint: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - - TOXENV=checkqa - - UPLOAD_COVERAGE=0 - py36dj111: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV=py36-dj111 - py36dj22: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV=py36-dj22 - py36dj30: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - TOXENV=py36-dj30 - py37dj111: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV=py37-dj111 - py37dj22: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV=py37-dj22 - py37dj30: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - TOXENV=py37-dj30 - py38dj22: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV=py38-dj22 - py38dj30: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - TOXENV=py38-dj30 - -workflows: - version: 2 - test: - jobs: - - lint - - py36dj111 - - py36dj22 - - py36dj30 - - py37dj111 - - py37dj22 - - py37dj30 - - py38dj22 - - py38dj30 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..c846d887 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,68 @@ +name: Lints and Tests +on: [push] +jobs: + lint: + name: Linting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: isort + uses: jamescurtin/isort-action@master + + - name: Lints + uses: py-actions/flake8@v1 + with: + ignore: "E265,E501" + max-line-length: "100" + max-complexity: "10" + exclude: "account/migrations,docs" + inline-quotes: "double" + path: account + + + test: + name: Testing + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + django: [2.2.*, 3.0.*, 3.1.*] + include: + - python: 3.5 + django: 2.2.* + + steps: + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + + - name: Install Django and Testing Tools + run: pip install Django==${{ matrix.django }} coverage + + - name: Running Python Tests + env: + DJANGO_SETTINGS_MODULE: account.tests.settings + run: | + pip freeze + coverage run setup.py test + + - name: Generating Coverage Report Artifact + run: | + coverage html + zip coverage-html.zip htmlcov/* + + - name: Store Coverage Report Artifact + uses: actions/upload-artifact@v1 + with: + name: coverage-report + path: coverage-html.zip diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6085fb98 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[tool.isort] +profile = "hug" +src_paths = ["account"] +multi_line_output = 3 +known_django = "django" +known_third_party = "account,six,mock,appconf,jsonfield,pytz" +sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" +skip_glob = "account/migrations/*,docs" +include_trailing_comma = "True" diff --git a/setup.py b/setup.py index 44910262..fee001c7 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ url="http://github.com/pinax/django-user-accounts", packages=find_packages(), install_requires=[ - "Django>=1.11", - "django-appconf>=1.0.1", - "pytz>=2015.6" + "Django>=2.2", + "django-appconf>=1.0.4", + "pytz>=2020.4" ], zip_safe=False, package_data={ @@ -30,16 +30,18 @@ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", - 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ] ) diff --git a/tox.ini b/tox.ini index 991c734e..4d287a9b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,17 +2,9 @@ ignore = E265,E501 max-line-length = 100 max-complexity = 10 -exclude = account/migrations/*,docs/* +exclude = account/migrations,docs inline-quotes = double -[isort] -multi_line_output=3 -known_django=django -known_third_party=account,six,mock,appconf,jsonfield,pytz -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -skip_glob=migrations/* -include_trailing_comma=True - [coverage:run] source = account omit = account/conf.py,account/tests/*,account/migrations/* @@ -24,37 +16,3 @@ omit = account/conf.py,account/tests/*,account/migrations/* exclude_lines = coverage: omit show_missing = True - -[tox] -envlist = - checkqa - py{36,37}-dj{111,22,30}{,-pytest} - py38-dj{22,30}{,-pytest} - -[testenv] -passenv = CI CIRCLECI CIRCLE_* -deps = - coverage<5 - codecov - dj111: Django>=1.11,<1.12 - dj22: Django>=2.2,<3.0 - dj30: Django>=3.0,<3.1 - master: https://github.com/django/django/tarball/master -extras = - pytest: pytest -usedevelop = True -setenv = - DJANGO_SETTINGS_MODULE=account.tests.settings - pytest: _ACCOUNT_TEST_RUNNER=-m pytest -commands = - coverage run {env:_ACCOUNT_TEST_RUNNER:setup.py test} {posargs} - coverage report -m --skip-covered - -[testenv:checkqa] -commands = - flake8 account - isort --recursive --check-only --diff account -sp tox.ini -deps = - flake8 == 3.7.9 - flake8-quotes == 2.1.1 - isort == 4.3.21 From 789546c6d18001007891b9c6fb2e1dd1757488c5 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Mon, 21 Dec 2020 23:46:39 -0600 Subject: [PATCH 161/239] Fix isort --- makemigrations.py | 2 -- setup.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/makemigrations.py b/makemigrations.py index e6881231..eb2aa60d 100644 --- a/makemigrations.py +++ b/makemigrations.py @@ -3,10 +3,8 @@ import sys import django - from django.conf import settings - DEFAULT_SETTINGS = dict( INSTALLED_APPS=[ "django.contrib.auth", diff --git a/setup.py b/setup.py index fee001c7..0bfe1f4a 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -from setuptools import setup, find_packages - +from setuptools import find_packages, setup setup( name="django-user-accounts", From 5bd3bd817aad7a39c00e5ec7b9a140e16a9711b9 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Mon, 21 Dec 2020 23:57:32 -0600 Subject: [PATCH 162/239] Remove unepxected inputs --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c846d887..25fa9d1d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,9 +21,7 @@ jobs: with: ignore: "E265,E501" max-line-length: "100" - max-complexity: "10" exclude: "account/migrations,docs" - inline-quotes: "double" path: account From b5b59b647d4e5d063783d9915d0b5d0bf7926854 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 22 Dec 2020 00:04:25 -0600 Subject: [PATCH 163/239] Update README --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1462b29c..a9182354 100644 --- a/README.md +++ b/README.md @@ -54,17 +54,17 @@ Pinax is an open-source platform built on the Django Web Framework. It is an eco #### Supported Django and Python versions -Django / Python | 3.6 | 3.7 | 3.8 ---------------- | --- | --- | --- -1.11 | * | * | -2.2 | * | * | * -3.0 | * | * | * +Django / Python | 3.6 | 3.7 | 3.8 | 3.9 +--------------- | --- | --- | --- | --- +2.2 | * | * | * | * +3.0 | * | * | * | * +3.1 | * | * | * | * ## Requirements -* Django 1.11, 2.2, or 3.0 -* Python 3.6, 3.7, or 3.8 +* Django 2.2, 3.0, or 3.1 +* Python 3.6, 3.7, 3.8, or 3.9 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) From 73f5b853ffc905d695e690851e9f9a469f2f51f2 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Tue, 22 Dec 2020 08:28:43 -0600 Subject: [PATCH 164/239] Drop deprecated providing_args argument from signals --- account/signals.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/account/signals.py b/account/signals.py index a648193b..88df377c 100644 --- a/account/signals.py +++ b/account/signals.py @@ -1,12 +1,12 @@ import django.dispatch -user_signed_up = django.dispatch.Signal(providing_args=["user", "form"]) -user_sign_up_attempt = django.dispatch.Signal(providing_args=["username", "email", "result"]) -user_logged_in = django.dispatch.Signal(providing_args=["user", "form"]) -user_login_attempt = django.dispatch.Signal(providing_args=["username", "result"]) -signup_code_sent = django.dispatch.Signal(providing_args=["signup_code"]) -signup_code_used = django.dispatch.Signal(providing_args=["signup_code_result"]) -email_confirmed = django.dispatch.Signal(providing_args=["email_address"]) -email_confirmation_sent = django.dispatch.Signal(providing_args=["confirmation"]) -password_changed = django.dispatch.Signal(providing_args=["user"]) -password_expired = django.dispatch.Signal(providing_args=["user"]) +user_signed_up = django.dispatch.Signal() +user_sign_up_attempt = django.dispatch.Signal() +user_logged_in = django.dispatch.Signal() +user_login_attempt = django.dispatch.Signal() +signup_code_sent = django.dispatch.Signal() +signup_code_used = django.dispatch.Signal() +email_confirmed = django.dispatch.Signal() +email_confirmation_sent = django.dispatch.Signal() +password_changed = django.dispatch.Signal() +password_expired = django.dispatch.Signal() From 126261d759240febabd72e040cb18db24eb5a20e Mon Sep 17 00:00:00 2001 From: Trevor Date: Wed, 7 Apr 2021 11:38:58 -0400 Subject: [PATCH 165/239] update ref to TEMPLATE_CONTEXT_PROCESSORS --- docs/installation.rst | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 44d09223..2f83a991 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -27,12 +27,25 @@ Add ``account.urls`` to your URLs definition:: ... ) -Add ``account.context_processors.account`` to ``TEMPLATE_CONTEXT_PROCESSORS``:: - - TEMPLATE_CONTEXT_PROCESSORS = [ - ... - "account.context_processors.account", - ... +Add ``account.context_processors.account`` to ``context_processors``:: + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + + # add django-user-accounts context processor + 'account.context_processors.account', + ], + }, + }, ] Add ``account.middleware.LocaleMiddleware`` and From 82f1ee4bbef000fe64b1bab2ef72bac3e8ea44b9 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 24 Apr 2021 16:38:15 -0500 Subject: [PATCH 166/239] Update urls --- account/urls.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/account/urls.py b/account/urls.py index 3a52e728..6ee8ed4f 100644 --- a/account/urls.py +++ b/account/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from account.views import ( ChangePasswordView, @@ -13,13 +13,13 @@ ) urlpatterns = [ - url(r"^signup/$", SignupView.as_view(), name="account_signup"), - url(r"^login/$", LoginView.as_view(), name="account_login"), - url(r"^logout/$", LogoutView.as_view(), name="account_logout"), - url(r"^confirm_email/(?P\w+)/$", ConfirmEmailView.as_view(), name="account_confirm_email"), - url(r"^password/$", ChangePasswordView.as_view(), name="account_password"), - url(r"^password/reset/$", PasswordResetView.as_view(), name="account_password_reset"), - url(r"^password/reset/(?P[0-9A-Za-z]+)-(?P.+)/$", PasswordResetTokenView.as_view(), name="account_password_reset_token"), - url(r"^settings/$", SettingsView.as_view(), name="account_settings"), - url(r"^delete/$", DeleteView.as_view(), name="account_delete"), + path("signup/", SignupView.as_view(), name="account_signup"), + path("login/", LoginView.as_view(), name="account_login"), + path("logout/", LogoutView.as_view(), name="account_logout"), + path("confirm_email//", ConfirmEmailView.as_view(), name="account_confirm_email"), + path("password/", ChangePasswordView.as_view(), name="account_password"), + path("password/reset/", PasswordResetView.as_view(), name="account_password_reset"), + path("password/reset///$", PasswordResetTokenView.as_view(), name="account_password_reset_token"), + path("settings/", SettingsView.as_view(), name="account_settings"), + path("delete/", DeleteView.as_view(), name="account_delete"), ] From 39ca87430ede12626f96bb5b5da0791f23c1c43d Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 24 Apr 2021 16:51:42 -0500 Subject: [PATCH 167/239] Drop support for 3.0 and add 3.2 --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25fa9d1d..181ee088 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,7 @@ jobs: lint: name: Linting runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v2 @@ -12,7 +12,7 @@ jobs: uses: actions/setup-python@v1 with: python-version: 3.9 - + - name: isort uses: jamescurtin/isort-action@master @@ -31,11 +31,11 @@ jobs: strategy: matrix: python: [3.6, 3.7, 3.8, 3.9] - django: [2.2.*, 3.0.*, 3.1.*] + django: [2.2.*, 3.1.*, 3.2.*] include: - python: 3.5 django: 2.2.* - + steps: - uses: actions/checkout@v2 From 0980b75dccc9829c9a6f11e162e51f3c31370dbb Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 24 Apr 2021 16:57:46 -0500 Subject: [PATCH 168/239] v3.0.3 --- CHANGELOG.md | 5 +++++ setup.py | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a07fd1..cd58f788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ version with these. Your code will need to be updated to continue working. ## 3.0.3 +* Fix deprecated urls +* Update template context processors docs +* Fix deprecrated argument in signals +* Update decorators for Django 3 +* Fix issue with lazy string * Drop deprecated `force_text()` ## 3.0.2 diff --git a/setup.py b/setup.py index 7d2bbb17..5b22ae36 100644 --- a/setup.py +++ b/setup.py @@ -30,17 +30,12 @@ "Environment :: Web Environment", "Framework :: Django", 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', + 'Framework :: Django :: 3.2', "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', ] ) From 0b67aecd2347dd75ea9bcc9ed05c275ca2ddd8d5 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 24 Apr 2021 17:35:26 -0500 Subject: [PATCH 169/239] 3.0.4 - missed a thing --- account/urls.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/account/urls.py b/account/urls.py index 6ee8ed4f..f6eabecd 100644 --- a/account/urls.py +++ b/account/urls.py @@ -19,7 +19,7 @@ path("confirm_email//", ConfirmEmailView.as_view(), name="account_confirm_email"), path("password/", ChangePasswordView.as_view(), name="account_password"), path("password/reset/", PasswordResetView.as_view(), name="account_password_reset"), - path("password/reset///$", PasswordResetTokenView.as_view(), name="account_password_reset_token"), + path("password/reset///", PasswordResetTokenView.as_view(), name="account_password_reset_token"), path("settings/", SettingsView.as_view(), name="account_settings"), path("delete/", DeleteView.as_view(), name="account_delete"), ] diff --git a/setup.py b/setup.py index 5b22ae36..aa4a6da8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="django-user-accounts", - version="3.0.3", + version="3.0.4", author="Brian Rosner", author_email="brosner@gmail.com", description="a Django user account app", From 42dc143a2ff819f767d566cbca4207b1bc30645d Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 24 Apr 2021 18:48:02 -0500 Subject: [PATCH 170/239] Drop Python 3.5 from test matrix --- .github/workflows/ci.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 181ee088..80ab0530 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,9 +32,6 @@ jobs: matrix: python: [3.6, 3.7, 3.8, 3.9] django: [2.2.*, 3.1.*, 3.2.*] - include: - - python: 3.5 - django: 2.2.* steps: - uses: actions/checkout@v2 From 0302388419b6bcae88ffdd229893d4ecdef562fe Mon Sep 17 00:00:00 2001 From: Jonathan Sundqvist Date: Sun, 11 Jul 2021 19:18:57 +0200 Subject: [PATCH 171/239] Use the same max_length as the username field --- account/forms.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/account/forms.py b/account/forms.py index b093df75..1b0c3714 100644 --- a/account/forms.py +++ b/account/forms.py @@ -14,6 +14,9 @@ alnum_re = re.compile(r"^\w+$") +User = get_user_model() +USER_FIELD_MAX_LENGTH = User._meta.get_field(User.USERNAME_FIELD).max_length + class PasswordField(forms.CharField): @@ -35,7 +38,7 @@ class SignupForm(forms.Form): username = forms.CharField( label=_("Username"), - max_length=30, + max_length=USER_FIELD_MAX_LENGTH, widget=forms.TextInput(), required=True ) @@ -60,7 +63,6 @@ class SignupForm(forms.Form): def clean_username(self): if not alnum_re.search(self.cleaned_data["username"]): raise forms.ValidationError(_("Usernames can only contain letters, numbers and underscores.")) - User = get_user_model() lookup_kwargs = get_user_lookup_kwargs({ "{username}__iexact": self.cleaned_data["username"] }) @@ -114,7 +116,7 @@ def user_credentials(self): class LoginUsernameForm(LoginForm): - username = forms.CharField(label=_("Username"), max_length=30) + username = forms.CharField(label=_("Username"), max_length=USER_FIELD_MAX_LENGTH) authentication_fail_message = _("The username and/or password you specified are not correct.") identifier_field = "username" From 14b429d3f48437792d2a229d8deae207a911814d Mon Sep 17 00:00:00 2001 From: Jonathan Sundqvist Date: Sun, 11 Jul 2021 10:41:07 +0200 Subject: [PATCH 172/239] Make the default http protocol https As any login site should be using https in this day and age https should be used as a default, furthermore this won't really affect local development as the links are in an email. Making it https by default prevents the developer of forgetting this easy to miss setting. --- account/conf.py | 1 + account/models.py | 4 ++-- account/views.py | 4 ++-- docs/settings.rst | 6 ++++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/account/conf.py b/account/conf.py index f36e90db..ad46655e 100644 --- a/account/conf.py +++ b/account/conf.py @@ -49,6 +49,7 @@ class AccountAppConf(AppConf): SETTINGS_REDIRECT_URL = "account_settings" NOTIFY_ON_PASSWORD_CHANGE = True DELETION_EXPUNGE_HOURS = 48 + DEFAULT_HTTP_PROTOCOL = 'https' HOOKSET = "account.hooks.AccountDefaultHookSet" TIMEZONES = TIMEZONES LANGUAGES = LANGUAGES diff --git a/account/models.py b/account/models.py index 7d479a11..1c92e5bb 100644 --- a/account/models.py +++ b/account/models.py @@ -207,7 +207,7 @@ def use(self, user): signup_code_used.send(sender=result.__class__, signup_code_result=result) def send(self, **kwargs): - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") + protocol = settings.ACCOUNT_DEFAULT_HTTP_PROTOCOL current_site = kwargs["site"] if "site" in kwargs else Site.objects.get_current() if "signup_url" not in kwargs: signup_url = "{0}://{1}{2}?{3}".format( @@ -328,7 +328,7 @@ def confirm(self): def send(self, **kwargs): current_site = kwargs["site"] if "site" in kwargs else Site.objects.get_current() - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") + protocol = settings.ACCOUNT_DEFAULT_HTTP_PROTOCOL activate_url = "{0}://{1}{2}".format( protocol, current_site.domain, diff --git a/account/views.py b/account/views.py index 8f86749a..109bc79b 100644 --- a/account/views.py +++ b/account/views.py @@ -101,7 +101,7 @@ def get_success_url(self, fallback_url=None, **kwargs): return default_redirect(self.request, fallback_url, **kwargs) def send_password_email(self, user): - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") + protocol = settings.ACCOUNT_DEFAULT_HTTP_PROTOCOL current_site = get_current_site(self.request) ctx = { "user": user, @@ -632,7 +632,7 @@ def form_valid(self, form): def send_email(self, email): User = get_user_model() - protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") + protocol = settings.ACCOUNT_DEFAULT_HTTP_PROTOCOL current_site = get_current_site(self.request) email_qs = EmailAddress.objects.filter(email__iexact=email) for user in User.objects.filter(pk__in=email_qs.values("user")): diff --git a/docs/settings.rst b/docs/settings.rst index 6cb76db3..d6fcd03d 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -125,6 +125,12 @@ Default: ``"account.callbacks.account_delete_expunge"`` Default: ``48`` + +``ACCOUNT_DEFAULT_HTTP_PROTOCOL`` +================================= + +Default: ``https`` + ``ACCOUNT_HOOKSET`` =================== From 11fca8dc8e01b557d151af6136a240255b7601e8 Mon Sep 17 00:00:00 2001 From: Jonathan Sundqvist Date: Sun, 11 Jul 2021 21:58:06 +0200 Subject: [PATCH 173/239] Introduce our own ModelBackend to reduce queries --- account/auth_backends.py | 26 +++++++++++++++++++++++--- account/models.py | 7 +++---- docs/installation.rst | 7 +++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index d3777edb..a1d759d3 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -6,13 +6,31 @@ from account.utils import get_user_lookup_kwargs -class UsernameAuthenticationBackend(ModelBackend): +User = get_user_model() + + +class AccountModelBackend(ModelBackend): + """ + This authentication backend ensures that the account is always selected + on any query with the user, so we don't issue extra unnecessary queries + """ + + def get_user(self, user_id): + """Get the user and select account at the same time""" + user = User._default_manager.filter(pk=user_id).select_related('account').first() + if not user: + return None + return user if self.user_can_authenticate(user) else None + + +class UsernameAuthenticationBackend(AccountModelBackend): + """Username authentication""" def authenticate(self, request, username=None, password=None, **kwargs): + """Authenticate the user based on user""" if username is None or password is None: return None - User = get_user_model() try: lookup_kwargs = get_user_lookup_kwargs({ "{username}__iexact": username @@ -25,9 +43,11 @@ def authenticate(self, request, username=None, password=None, **kwargs): return user -class EmailAuthenticationBackend(ModelBackend): +class EmailAuthenticationBackend(AccountModelBackend): + """Email authentication""" def authenticate(self, request, username=None, password=None, **kwargs): + """Authenticate the user based email""" qs = EmailAddress.objects.filter(Q(primary=True) | Q(verified=True)) if username is None or password is None: diff --git a/account/models.py b/account/models.py index 7d479a11..01eee91f 100644 --- a/account/models.py +++ b/account/models.py @@ -38,10 +38,9 @@ class Account(models.Model): def for_request(cls, request): user = getattr(request, "user", None) if user and user.is_authenticated: - try: - return Account._default_manager.get(user=user) - except Account.DoesNotExist: - pass + account = user.account + if account: + return account return AnonymousAccount(request) @classmethod diff --git a/docs/installation.rst b/docs/installation.rst index 2f83a991..32ff4afd 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -67,6 +67,13 @@ Optionally include ``account.middleware.ExpiredPasswordMiddleware`` in ... ] +Set the authentication backends to the following:: + + AUTHENTICATION_BACKENDS = [ + 'account.auth_backends.AccountModelBackend', + 'django.contrib.auth.backends.ModelBackend' + ] + Once everything is in place make sure you run ``migrate`` to modify the database with the ``account`` app models. From 6451bb9ba84097b42e80ba38a6ccab7b0401a2d2 Mon Sep 17 00:00:00 2001 From: Yogesh Dwivedi <77800350+Yogeshddjango@users.noreply.github.com> Date: Fri, 1 Oct 2021 15:42:27 +0530 Subject: [PATCH 174/239] Update utils.py --- account/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/utils.py b/account/utils.py index 4163170b..b60b5e3b 100644 --- a/account/utils.py +++ b/account/utils.py @@ -131,7 +131,7 @@ def check_password_expired(user): except PasswordHistory.DoesNotExist: return False - now = datetime.datetime.now(tz=pytz.UTC) + now = datetime.datetime.now() expiration = latest.timestamp + datetime.timedelta(seconds=expiry) if expiration < now: From 153277e68b8f3cdd79326628e468ef0a9fa03418 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 14:51:08 -0600 Subject: [PATCH 175/239] Updates to CI and packaging --- .github/workflows/ci.yaml | 28 ++++++++---------- account/tests/settings.py | 1 + account/tests/test_email_address.py | 1 - pyproject.toml | 7 +++++ runtests.py | 13 +++------ setup.cfg | 44 +++++++++++++++++++++++++++++ setup.py | 41 --------------------------- 7 files changed, 67 insertions(+), 68 deletions(-) delete mode 100644 setup.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 80ab0530..897f2293 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,8 +30,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.6, 3.7, 3.8, 3.9] - django: [2.2.*, 3.1.*, 3.2.*] + python: [3.6, 3.7, 3.8, 3.9, "3.10"] + django: [2.2.*, 3.2.*] steps: - uses: actions/checkout@v2 @@ -42,22 +42,16 @@ jobs: python-version: ${{ matrix.python }} - name: Install Django and Testing Tools - run: pip install Django==${{ matrix.django }} coverage + run: | + pip install Django==${{ matrix.django }} coverage pytest + pip install . - name: Running Python Tests - env: - DJANGO_SETTINGS_MODULE: account.tests.settings - run: | - pip freeze - coverage run setup.py test + run: coverage run runtests.py - - name: Generating Coverage Report Artifact + - name: Upload Coverage Report + if: matrix.django == '3.2.*' && matrix.python == '3.10' run: | - coverage html - zip coverage-html.zip htmlcov/* - - - name: Store Coverage Report Artifact - uses: actions/upload-artifact@v1 - with: - name: coverage-report - path: coverage-html.zip + pip install codecov + coverage xml + codecov --required -X search gcov pycov -f coverage.xml diff --git a/account/tests/settings.py b/account/tests/settings.py index 1fffaf44..6ec3bbf8 100644 --- a/account/tests/settings.py +++ b/account/tests/settings.py @@ -45,3 +45,4 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware" ] +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/account/tests/test_email_address.py b/account/tests/test_email_address.py index 673e33a7..f5aa3b1f 100644 --- a/account/tests/test_email_address.py +++ b/account/tests/test_email_address.py @@ -1,5 +1,4 @@ from account.models import EmailAddress -from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.forms import ValidationError from django.test import TestCase, override_settings diff --git a/pyproject.toml b/pyproject.toml index 6085fb98..d98fb179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,10 @@ known_third_party = "account,six,mock,appconf,jsonfield,pytz" sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" skip_glob = "account/migrations/*,docs" include_trailing_comma = "True" + +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/runtests.py b/runtests.py index e64d90ec..9b7c02de 100644 --- a/runtests.py +++ b/runtests.py @@ -12,15 +12,10 @@ def runtests(*test_args): parent = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, parent) - try: - from django.test.runner import DiscoverRunner - runner_class = DiscoverRunner - if not test_args: - test_args = ["account.tests"] - except ImportError: - from django.test.simple import DjangoTestSuiteRunner - runner_class = DjangoTestSuiteRunner - test_args = ["tests"] + from django.test.runner import DiscoverRunner + runner_class = DiscoverRunner + if not test_args: + test_args = ["account.tests"] failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) sys.exit(failures) diff --git a/setup.cfg b/setup.cfg index 5037aeb7..9eaabe46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,47 @@ [tool:pytest] testpaths = account/tests DJANGO_SETTINGS_MODULE = account.tests.settings + +[metadata] +name = django-user-accounts +version = 4.0.0 +author = Pinax Team +author_email = team@pinaxproject.com +description = a Django user account app +long_description = file: README.md +long_description_content_type = text/markdown +license = MIT, +url = http://github.com/pinax/django-user-accounts +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Web Environment + Framework :: Django + Framework :: Django :: 2.2 + Framework :: Django :: 3.2 + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[options] +package_dir = + = account +packages = find: +include_package_data = True +install_requires = + Django>=2.2 + django-appconf>=1.0.4 + pytz>=2020.4 +zip_safe = False + +[options.packages.find] +where = account + +[options.package_data] +account = locale/*/LC_MESSAGES/* diff --git a/setup.py b/setup.py deleted file mode 100644 index aa4a6da8..00000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="django-user-accounts", - version="3.0.4", - author="Brian Rosner", - author_email="brosner@gmail.com", - description="a Django user account app", - long_description=open("README.md").read(), - license="MIT", - url="http://github.com/pinax/django-user-accounts", - packages=find_packages(), - install_requires=[ - "Django>=2.2", - "django-appconf>=1.0.4", - "pytz>=2020.4" - ], - zip_safe=False, - package_data={ - "account": [ - "locale/*/LC_MESSAGES/*", - ], - }, - extras_require={ - "pytest": ["pytest", "pytest-django"] - }, - test_suite="runtests.runtests", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Django", - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - 'Programming Language :: Python :: 3', - ] -) From f85067782ee224c1c47f13df278a54520b538f91 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 14:53:58 -0600 Subject: [PATCH 176/239] Lints --- account/auth_backends.py | 2 +- account/conf.py | 2 +- account/models.py | 2 +- account/tests/test_decorators.py | 2 +- account/tests/test_models.py | 30 +++++++++++++++--------------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index a1d759d3..d5f58768 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -17,7 +17,7 @@ class AccountModelBackend(ModelBackend): def get_user(self, user_id): """Get the user and select account at the same time""" - user = User._default_manager.filter(pk=user_id).select_related('account').first() + user = User._default_manager.filter(pk=user_id).select_related("account").first() if not user: return None return user if self.user_can_authenticate(user) else None diff --git a/account/conf.py b/account/conf.py index 81111f3b..ff711b1f 100644 --- a/account/conf.py +++ b/account/conf.py @@ -50,7 +50,7 @@ class AccountAppConf(AppConf): SETTINGS_REDIRECT_URL = "account_settings" NOTIFY_ON_PASSWORD_CHANGE = True DELETION_EXPUNGE_HOURS = 48 - DEFAULT_HTTP_PROTOCOL = 'https' + DEFAULT_HTTP_PROTOCOL = "https" HOOKSET = "account.hooks.AccountDefaultHookSet" TIMEZONES = TIMEZONES LANGUAGES = LANGUAGES diff --git a/account/models.py b/account/models.py index 5235d73c..5e7080fc 100644 --- a/account/models.py +++ b/account/models.py @@ -297,7 +297,7 @@ def validate_unique(self, exclude=None): if qs.exists() and settings.ACCOUNT_EMAIL_UNIQUE: raise forms.ValidationError({ - 'email': _("A user is registered with this email address."), + "email": _("A user is registered with this email address."), }) diff --git a/account/tests/test_decorators.py b/account/tests/test_decorators.py index 213d4296..06c62240 100644 --- a/account/tests/test_decorators.py +++ b/account/tests/test_decorators.py @@ -8,7 +8,7 @@ @login_required def mock_view(request, *args, **kwargs): - return HttpResponse('OK', status=200) + return HttpResponse("OK", status=200) class LoginRequiredDecoratorTestCase(TestCase): diff --git a/account/tests/test_models.py b/account/tests/test_models.py index dd91315d..0f9cf67a 100644 --- a/account/tests/test_models.py +++ b/account/tests/test_models.py @@ -5,41 +5,41 @@ class SignupCodeModelTestCase(TestCase): def test_exists_no_match(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertFalse(SignupCode.exists(code='BARBAR')) - self.assertFalse(SignupCode.exists(email='bar@example.com')) - self.assertFalse(SignupCode.exists(email='bar@example.com', code='BARBAR')) + self.assertFalse(SignupCode.exists(code="BARBAR")) + self.assertFalse(SignupCode.exists(email="bar@example.com")) + self.assertFalse(SignupCode.exists(email="bar@example.com", code="BARBAR")) self.assertFalse(SignupCode.exists()) def test_exists_email_only_match(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertTrue(SignupCode.exists(email='foobar@example.com')) + self.assertTrue(SignupCode.exists(email="foobar@example.com")) def test_exists_code_only_match(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertTrue(SignupCode.exists(code='FOOFOO')) - self.assertTrue(SignupCode.exists(email='bar@example.com', code='FOOFOO')) + self.assertTrue(SignupCode.exists(code="FOOFOO")) + self.assertTrue(SignupCode.exists(email="bar@example.com", code="FOOFOO")) def test_exists_email_match_code_mismatch(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertTrue(SignupCode.exists(email='foobar@example.com', code='BARBAR')) + self.assertTrue(SignupCode.exists(email="foobar@example.com", code="BARBAR")) def test_exists_code_match_email_mismatch(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertTrue(SignupCode.exists(email='bar@example.com', code='FOOFOO')) + self.assertTrue(SignupCode.exists(email="bar@example.com", code="FOOFOO")) def test_exists_both_match(self): - code = SignupCode(email='foobar@example.com', code='FOOFOO') + code = SignupCode(email="foobar@example.com", code="FOOFOO") code.save() - self.assertTrue(SignupCode.exists(email='foobar@example.com', code='FOOFOO')) + self.assertTrue(SignupCode.exists(email="foobar@example.com", code="FOOFOO")) From 157c12da646f0c7e7a0c316a4be75a1ad5d480ef Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 14:55:34 -0600 Subject: [PATCH 177/239] More lints --- account/auth_backends.py | 1 - account/tests/test_email_address.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/account/auth_backends.py b/account/auth_backends.py index d5f58768..a93f15a0 100644 --- a/account/auth_backends.py +++ b/account/auth_backends.py @@ -5,7 +5,6 @@ from account.models import EmailAddress from account.utils import get_user_lookup_kwargs - User = get_user_model() diff --git a/account/tests/test_email_address.py b/account/tests/test_email_address.py index f5aa3b1f..4f47977e 100644 --- a/account/tests/test_email_address.py +++ b/account/tests/test_email_address.py @@ -1,8 +1,9 @@ -from account.models import EmailAddress from django.contrib.auth.models import User from django.forms import ValidationError from django.test import TestCase, override_settings +from account.models import EmailAddress + @override_settings(ACCOUNT_EMAIL_UNIQUE=True) class UniqueEmailAddressTestCase(TestCase): From cf4eb6786d4f8983b03282ba27aabb79f7832e23 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 15:18:22 -0600 Subject: [PATCH 178/239] Add doc updates from #210 Thanks @jantoniomartin --- docs/installation.rst | 4 +- docs/settings.rst | 92 ++++++++++++---- docs/templates.rst | 240 +++++++++++++++++++++++++++++++++++++----- docs/usage.rst | 45 +++----- 4 files changed, 305 insertions(+), 76 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 32ff4afd..28388934 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -8,9 +8,11 @@ Install the development version:: pip install django-user-accounts -Add ``account`` to your ``INSTALLED_APPS`` setting:: +Make sure that ``django.contrib.sites`` is in ``INSTALLED_APPS`` and add + ``account`` to this setting:::: INSTALLED_APPS = ( + "django.contrib.sites", # ... "account", # ... diff --git a/docs/settings.rst b/docs/settings.rst index acbd1e36..a6988711 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -9,132 +9,169 @@ Settings Default: ``True`` +If ``True``, creation of new accounts is allowed. When the signup view is +called, the template ``account/signup.html`` will be displayed, usually +showing a form to collect the new user data. + +If ``False``, creation of new accounts is disabled. When the signup view is +called, the template ``account/signup_closed.html`` will be displayed. + ``ACCOUNT_LOGIN_URL`` ===================== Default: ``"account_login"`` +The name of the urlconf that calls the login view. + ``ACCOUNT_SIGNUP_REDIRECT_URL`` =============================== Default: ``"/"`` +The url where the user will be redirected after a successful signup. + ``ACCOUNT_LOGIN_REDIRECT_URL`` ============================== Default: ``"/"`` +The url where the user will be redirected after a successful authentication, +unless the ``next`` parameter is defined in the request. + ``ACCOUNT_LOGOUT_REDIRECT_URL`` =============================== Default: ``"/"`` +The url where the user will be redirected after logging out. ``ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL`` ======================================== Default: ``"account_password"`` +The url where the user will be redirected after changing his password. + ``ACCOUNT_PASSWORD_RESET_REDIRECT_URL`` ======================================= Default: ``"account_login"`` -``ACCOUNT_PASSWORD_RESET_TOKEN_URL`` -==================================== - -Default: ``"account_password_reset_token"`` - -``ACCOUNT_PASSWORD_EXPIRY`` -=========================== - -Default: ``0`` - -``ACCOUNT_PASSWORD_USE_HISTORY`` -================================ - -Default: ``False`` +The url where the user will be redirected after resetting his password. ``ACCOUNT_REMEMBER_ME_EXPIRY`` ============================== Default: ``60 * 60 * 24 * 365 * 10`` +The number of seconds that the user will remain authenticated after he logs in +the site. + ``ACCOUNT_USER_DISPLAY`` ======================== Default: ``lambda user: user.username`` +The function that will be called by the template tag user_display. + ``ACCOUNT_CREATE_ON_SAVE`` ========================== Default: ``True`` +If ``True``, an account instance will be created when a new user is created. + ``ACCOUNT_EMAIL_UNIQUE`` ======================== Default: ``True`` +If ``False``, more than one user can have the same email address. + ``ACCOUNT_EMAIL_CONFIRMATION_REQUIRED`` ======================================= Default: ``False`` +If ``True``, new user accounts will be created as inactive. The user must use +the activation link to activate his account. + ``ACCOUNT_EMAIL_CONFIRMATION_EMAIL`` ==================================== Default: ``True`` +If ``True``, an email confirmation message will be sent to the user when they +make a new account. + ``ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS`` ========================================== Default: ``3`` +After this time, the email confirmation link will not be longer valid. + ``ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL`` ===================================================== Default: ``"account_login"`` +A urlconf name where the user will be redirected after confirming an email +address, if he is not authenticated. + ``ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL`` ========================================================= Default: ``None`` +A urlconf name where the user will be redirected after confirming an email +address, if he is authenticated. If not set, this url will be the one defined +in ``ACCOUNT_LOGIN_REDIRECT_URL``. + ``ACCOUNT_EMAIL_CONFIRMATION_URL`` ================================== Default: ``"account_confirm_email"`` +A urlconf name that will be used to confirm the user email (usually from the +email message they received). + ``ACCOUNT_SETTINGS_REDIRECT_URL`` ================================= Default: ``"account_settings"`` +The url where the user will be redirected after updating their account settings. + ``ACCOUNT_NOTIFY_ON_PASSWORD_CHANGE`` ===================================== Default: ``True`` +If ``True``, an notification email will be sent whenever a user changes their +password. + ``ACCOUNT_DELETION_MARK_CALLBACK`` ================================== Default: ``"account.callbacks.account_delete_mark"`` +This function will be called just after a user asks for account deletion. + ``ACCOUNT_DELETION_EXPUNGE_CALLBACK`` ===================================== Default: ``"account.callbacks.account_delete_expunge"`` +The function that will be called to expunge accounts. + ``ACCOUNT_DELETION_EXPUNGE_HOURS`` ================================== Default: ``48`` - -``ACCOUNT_DEFAULT_HTTP_PROTOCOL`` -================================= - -Default: ``https`` +The minimum time in hours since a user asks for account deletion until their +account is deleted. ``ACCOUNT_HOOKSET`` =================== @@ -155,7 +192,24 @@ override the following methods: Default: ``list(zip(pytz.all_timezones, pytz.all_timezones))`` +A list of time zones available for the user to set as their current time zone. + ``ACCOUNT_LANGUAGES`` ===================== +A tuple of languages available for the user to set as their preferred language. + See full list in: https://github.com/pinax/django-user-accounts/blob/master/account/languages.py + +``ACCOUNT_USE_AUTH_AUTHENTICATE`` +================================= + +Default: ``False`` + +If ``True``, ``django.contrib.auth.authenticate`` will be used to authenticate +the user. + +.. note:: + According to the comments in the code, this setting is deprecated and, + in the future, ``django.contrib.auth.authenticate`` will be the preferred + method. diff --git a/docs/templates.rst b/docs/templates.rst index 61c14994..550a033a 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -19,41 +19,229 @@ don't use ``pinax-theme-bootstrap``, then you will have to create these templates yourself. -Login/Registration/Signup Templates:: +Login/Registration/Signup Templates +----------------------------------- - account/login.html - account/logout.html - account/signup.html - account/signup_closed.html +**account/login.html** -Email Confirmation Templates:: +The template with the form to authenticate the user. The template has the +following context: - account/email_confirm.html - account/email_confirmation_sent.html - account/email_confirmed.html +``form`` + The login form. -Password Management Templates:: +``redirect_field_name`` + The name of the hidden field that will hold the url where to redirect the +user after login. - account/password_change.html - account/password_reset.html - account/password_reset_sent.html - account/password_reset_token.html - account/password_reset_token_fail.html +``redirect_field_value`` + The actual url where the user will be redirected after login. -Account Settings:: +**account/logout.html** - account/settings.html +The default template shown after the user has been logged out. -Emails (actual emails themselves):: +**account/signup.html** - account/email/email_confirmation_message.txt - account/email/email_confirmation_subject.txt - account/email/invite_user.txt - account/email/invite_user_subject.txt - account/email/password_change.txt - account/email/password_change_subject.txt - account/email/password_reset.txt - account/email/password_reset_subject.txt +The template with the form to registrate a new user. The template has the +following context: + +``form`` + The form used to create the new user. + +``redirect_field_name`` + The name of the hidden field that will hold the url where to redirect the +user after signing up. + +``redirect_field_value`` + The actual url where the user will be redirected after signing up. + +**account/signup_closed.html** + +A template to inform the user that creating new users is not allowed (mainly +because ``settings.ACCOUNT_OPEN_SIGNUP`` is ``False``). + +Email Confirmation Templates +---------------------------- + +**account/email_confirm.html** + +A template to confirm an email address. The template has the following context: + +``email`` + The email address where the activation link has been sent. + +``confirmation`` + The EmailConfirmation instance to be confirmed. + +**account/email_confirmation_sent.html** + +The template shown after a new user has been created. It should tell the user +that an activation link has been sent to his email address. The template has +the following context: + +``email`` + The email address where the activation link has been sent. + +``success_url`` + A url where the user can be redirected from this page. For example to +show a link to go back. + +**account/email_confirmed.html** + +A template shown after an email address has been confirmed. The template +context is the same as in email_confirm.html. + +``email`` + The email address that has been confirmed. + +Password Management Templates +----------------------------- + +**account/password_change.html** + +The template that shows the form to change the user's password, when the user +is authenticated. The template has the following context: + +``form`` + The form to change the password. + +**account/password_reset.html** + +A template with a form to type an email address to reset a user's password. +The template has the following context: + +``form`` + The form to reset the password. + +**account/password_reset_sent.html** + +A template to inform the user that his password has been reset and that he +should receive an email with a link to create a new password. The template has +the following context: + +``form`` + An instance of ``PasswordResetForm``. Usually the fields of this form +must be hidden. + +``resend`` + If ``True`` it means that the reset link has been resent to the user. + +**account/password_reset_token.html** + +The template that shows the form to change the user's password. The user should +have come here following the link received to reset his password. The template +has the following context: + +``form`` + The form to set the new password. + +**account/password_reset_token_fail.html** + +A template to inform the user that he is not allowed to change the password, +because the authentication token is wrong. The template has the following +context: + +``url`` + The url to request a new reset token. + +Account Settings +---------------- + +**account/settings.html** + +A template with a form where the user may change his email address, time zone +and preferred language. The template has the following context: + +``form`` + The form to change the settings. + +Emails (actual emails themselves) +--------------------------------- + +**account/email/email_confirmation_subject.txt** + +The subject line of the email that will be sent to the new user to validate the +email address. It will be rendered as a single line. The template has the +following context: + +``email_address`` + The actual email address where the activation message will be sent. + +``user`` + The new user object. + +``activate_url`` + The complete url for account activation, including protocol and domain. + +``current_site`` + The domain name of the site. + +``key`` + The confirmation key. + +**account/email/email_confirmation_message.txt** + +The body of the activation email. It has the same context as the subject +template (see above). + +**account/email/invite_user.txt** + +The body of the invitation sent to somebody to join the site. The template has +the following context: + +``signup_code`` + An instance of account.models.SignupCode. + +``current_site`` + The instance of django.contrib.sites.models.Site that identifies the site. + +``signup_url`` + The link used to use the invitation and create a new account. + +**account/email/invite_user_subject.txt** + +The subject line of the invitation sent to somebody to join the site. The +template has the same context as in invite_user.txt. + +**account/email/password_change.txt** + +The body of the email used to inform the user that his password has been +changed. The template has the following context: + +``user`` + The user whom the password belongs to. + +``protocol`` + The application protocol (usually http or https) being used in the site. + +``current_site`` + The instance of django.contrib.sites.models.Site that identifies the site. + +**account/email/password_change_subject.txt** + +The subject line of the email used to inform the user that his password has +been changed. The context is the same as in password_change.txt. + +**account/email/password_reset.txt** + +The body of the email with a link to reset a user's password. The template has +the following context: + + +``user`` + The user whom the password belongs to. + +``current_site`` + The instance of django.contrib.sites.models.Site that identifies the site. + +``password_reset_url`` + The link that the user needs to follow to set a new password. + +**account/email/password_reset_subject.txt** + +The subject line of the email with a link to reset a user's password. The +context is the same as in password_reset.txt. Template Tags ============= diff --git a/docs/usage.rst b/docs/usage.rst index b0abb043..dee40751 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -21,11 +21,6 @@ this app will: The rest of this document will cover how you can tweak the default behavior of django-user-accounts. -Limiting access to views -======================== - -To limit view access to logged in users, normally you would use the Django decorator ``django.contrib.auth.decorators.login_required``. Instead you should use ``account.decorators.login_required``. - Customizing the sign up process =============================== @@ -261,35 +256,25 @@ And in your settings:: TEST_RUNNER = "lib.tests.MyTestDiscoverRunner" +Restricting views to authenticated users +======================================== -Enabling password expiration -============================ - -Password expiration is disabled by default. In order to enable password expiration -you must add entries to your settings file:: - - ACCOUNT_PASSWORD_EXPIRY = 60*60*24*5 # seconds until pw expires, this example shows five days - ACCOUNT_PASSWORD_USE_HISTORY = True +``django.contrib.auth`` includes a convenient decorator and a mixin to restrict +views to authenticated users. ``django-user-accounts`` includes a modified +version of these decorator and mixin that should be used instead of the +usual ones. -and include `ExpiredPasswordMiddleware` with your middleware settings:: +If you want to restrict a function based view, use the decorator:: - MIDDLEWARE_CLASSES = { - ... - "account.middleware.ExpiredPasswordMiddleware", - } + from account.decorators import login_required -``ACCOUNT_PASSWORD_EXPIRY`` indicates the duration a password will stay valid. After that period -the password must be reset in order to view any page. If ``ACCOUNT_PASSWORD_EXPIRY`` is zero (0) -then passwords never expire. + @login_required + def restricted_view(request): + pass -If ``ACCOUNT_PASSWORD_USE_HISTORY`` is False, no history will be generated and password -expiration WILL NOT be checked. +To do the same with class based views, use the mixin:: -If ``ACCOUNT_PASSWORD_USE_HISTORY`` is True, a password history entry is created each time -the user changes their password. This entry links the user with their most recent -(encrypted) password and a timestamp. Unless deleted manually, PasswordHistory items -are saved forever, allowing password history checking for new passwords. + from account.mixins import LoginRequiredMixin -For an authenticated user, ``ExpiredPasswordMiddleware`` prevents retrieving or posting -to any page except the password change page and log out page when the user password is expired. -However, if the user is "staff" (can access the Django admin site), the password check is skipped. + class RestrictedView(LoginRequiredMixin, View): + pass From ccb0402458c8b8b147084d38213fc7654743974e Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 15:25:19 -0600 Subject: [PATCH 179/239] Allow overriding of password validation This closes PR #141. Applied manually because of rebase hell after such a long period. Thanks @rizumu --- account/forms.py | 10 ++++++---- account/hooks.py | 7 +++++++ docs/usage.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/account/forms.py b/account/forms.py index b093df75..6ac91e13 100644 --- a/account/forms.py +++ b/account/forms.py @@ -168,8 +168,9 @@ def clean_password_current(self): def clean_password_new_confirm(self): if "password_new" in self.cleaned_data and "password_new_confirm" in self.cleaned_data: - if self.cleaned_data["password_new"] != self.cleaned_data["password_new_confirm"]: - raise forms.ValidationError(_("You must type the same password each time.")) + password_new = self.cleaned_data["password_new"] + password_new_confirm = self.cleaned_data["password_new_confirm"] + return hookset.clean_password(password_new, password_new_confirm) return self.cleaned_data["password_new_confirm"] @@ -197,8 +198,9 @@ class PasswordResetTokenForm(forms.Form): def clean_password_confirm(self): if "password" in self.cleaned_data and "password_confirm" in self.cleaned_data: - if self.cleaned_data["password"] != self.cleaned_data["password_confirm"]: - raise forms.ValidationError(_("You must type the same password each time.")) + password = self.cleaned_data["password"] + password_confirm = self.cleaned_data["password_confirm"] + return hookset.clean_password(password, password_confirm) return self.cleaned_data["password_confirm"] diff --git a/account/hooks.py b/account/hooks.py index 606eee73..8cf2d1cf 100644 --- a/account/hooks.py +++ b/account/hooks.py @@ -1,8 +1,10 @@ import hashlib import random +from django import forms from django.core.mail import send_mail from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ from account.conf import settings @@ -53,6 +55,11 @@ def get_user_credentials(self, form, identifier_field): "password": form.cleaned_data["password"], } + def clean_password(self, password_new, password_new_confirm): + if password_new != password_new_confirm: + raise forms.ValidationError(_("You must type the same password each time.")) + return password_new + def account_delete_mark(self, deletion): deletion.user.is_active = False deletion.user.save() diff --git a/docs/usage.rst b/docs/usage.rst index dee40751..9d77ab22 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -278,3 +278,46 @@ To do the same with class based views, use the mixin:: class RestrictedView(LoginRequiredMixin, View): pass + + +Defining a custom password checker +================================== + +First add the path to the module which contains the +`AccountDefaultHookSet` subclass to your settings:: + + ACCOUNT_HOOKSET = "scenemachine.hooks.AccountHookSet" + +Then define a custom `clean_password` method on the `AccountHookSet` +class. + +Here is an example that harnesses the `VeryFacistCheck` dictionary +checker from `cracklib`_.:: + + import cracklib + + from django import forms from django.conf import settings from + django.template.defaultfilters import mark_safe from + django.utils.translation import ugettext_lazy as _ + + from account.hooks import AccountDefaultHookSet + + + class AccountHookSet(AccountDefaultHookSet): + + def clean_password(self, password_new, password_new_confirm): + password_new = super(AccountHookSet, self).clean_password(password_new, password_new_confirm) + try: + dictpath = "/usr/share/cracklib/pw_dict" + if dictpath: + cracklib.VeryFascistCheck(password_new, dictpath=dictpath) + else: + cracklib.VeryFascistCheck(password_new) + return password_new + except ValueError as e: + message = _(unicode(e)) + raise forms.ValidationError, mark_safe(message) + return password_new + + +.. _cracklib: https://pypi.python.org/pypi/cracklib/2.8.19 From 0f9203b0eaa2d0db646787da9d42e181d0b02abb Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 15:32:40 -0600 Subject: [PATCH 180/239] Updates Russian translations by @SnoUweR Closes #135 Updated manually because PR was so old that rebasing was a bear. --- account/locale/ru/LC_MESSAGES/django.mo | Bin 3711 -> 3766 bytes account/locale/ru/LC_MESSAGES/django.po | 45 +++++++++++++----------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/account/locale/ru/LC_MESSAGES/django.mo b/account/locale/ru/LC_MESSAGES/django.mo index c4c90a582fe14ccbf5a9085c8487fde59953f3af..6e7eb76c398743866391aec096754c6551e1c70a 100644 GIT binary patch delta 1053 zcma))T~8BH5Qc~PL0c#q6G0TiF;N>Nt*xLEG$vm76^t?92N8{KWTPati!C1}NJ?7; z4Pv9KH%6jS@2Sv*Qt0v%nDZZuG4aCq3-mo()EaNRdUns5dFP!u^P^#YXLff*!tWxw zvXMw5qLiEHAovOF2Df{Nc7Snk1DF9j!IvOH=nGf}eh2-a$4gWVhQS8#7`PAY0Z)VD zpwe3fcY$BMl%R9?@Kq6A1FwS3U;*q0KZB~kWvDiS1~>s`z!tF1N3;j*06&0t!OdW8 z4bc|R4+^k-jXj_;ejil24{Iukw&7!XuE{mC{$y())D{RI5TS5;OKWqeEfn&e)#3wz z3o$Jkk7xr%tX*7+XuW;-84~`ksBv*v@AQifV@QkWL*i&WY77p~ZK^y|k?0Pb(xZB; zlyu(Ed;11NICv;{*h~C^vs~n&oa99zr+A4iF36M|WBd0pB62veWKzg9&$A^sFO$52 zkR|zq7jS4HB`Gt4X9aF>D^n0!W-CZMZCY;4Z16NDikuZA`W>w=GK%wePN}qf%z4y? zT-K=yNjjloh~IMowHL}9mRtG9Ec`e+ww!>SNr47n6-9ZGb13F22FInSWb-GG5pbk=7HyA}Mi(qu4wU)h{C+gH z#QDEk!-Q0xPUr)Epw{c-zC$|dsy-j+O<_d zxJ4r+hq*N$e@6f2*Q}L0H6^5D4C)QwEGwVNlzH4&pGd;#qMX1w@+*VTX!o9z_=gfR(<~yaGf4cQvQ)1af zbZI@&MZ#+#@`8Er7Wft13;L`??cgl92fPDfi(Z3W;5)Dn{0KV0Z(uk08{7|WvJssF z$G{!n6R-z-Z=(c_BKU-bGoZyz)DQZ=X>bly2mAzgfmW#B1>N8Xm<2C@-@zB)$xb2{ z_!-;){s0ANcCN4&RE>{=O82a@gJ>&)>v?xucHNNIGjP~5ct{-d4i66X58$y)_#-nO zU)UdvO!;R*;bCziJn0XL^PxyE5Ei3X!U*y`9j8rZ;stHNx#hUvGV5}Vi=5|*Ov)7E z3f>0iSm(NsNg*G}6jyjj1aA8$rv#P@sz4@@w4{Bp1lo-a?UYrxo7`BFAk!jd?8S+n z;fSg_fO9e}=e3x1`_XYF=xGTGu5(d+bgps%1$E*qmt}@)#IICO2&)3EFI&5`G21@v zm@VouGS^DiZ0;j3RkeIf{2XNk5xW|?7Kj`A7_5l?>q?6L>pki{Z#2uy0WIhd zCh>2jO=j{H`-n;V<>*T=289h6#t, 2015 # Eugene MechanisM , 2012 msgid "" msgstr "" "Project-Id-Version: django-user-accounts\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-07-30 15:12-0600\n" -"PO-Revision-Date: 2015-04-23 15:27+0000\n" -"Last-Translator: Brian Rosner \n" -"Language-Team: Russian (http://www.transifex.com/projects/p/django-user-accounts/language/ru/)\n" +"PO-Revision-Date: 2014-08-12 01:34+0800\n" +"Last-Translator: Vladislav 'SnoUweR' Kovalev \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/django-user-" +"accounts/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: ru\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 1.6.7\n" #: forms.py:27 forms.py:107 msgid "Username" @@ -37,7 +39,8 @@ msgstr "Email" #: forms.py:52 msgid "Usernames can only contain letters, numbers and underscores." -msgstr "Имена пользователей могут состоять только из букв, цифр и подчеркиваний." +msgstr "" +"Имена пользователей могут состоять только из букв, цифер и подчеркиваний." #: forms.py:60 msgid "This username is already taken. Please choose another." @@ -45,15 +48,15 @@ msgstr "Это имя пользователя уже занято. Пожалу #: forms.py:67 forms.py:217 msgid "A user is registered with this email address." -msgstr "Пользователь зарегистрирован с этим email адресом." +msgstr "Данный электронный адрес уже используется в системе." #: forms.py:72 forms.py:162 forms.py:191 msgid "You must type the same password each time." -msgstr "Вы должны вводить одинаковый пароль каждый раз." +msgstr "Пароли не совпадают." #: forms.py:83 msgid "Remember Me" -msgstr "Запомнить Меня" +msgstr "Запомнить меня" #: forms.py:96 msgid "This account is inactive." @@ -61,31 +64,31 @@ msgstr "Этот аккаунт неактивен." #: forms.py:108 msgid "The username and/or password you specified are not correct." -msgstr "Имя пользователя или пароль некорректны." +msgstr "Имя пользователя и/или пароль введено некорректно." #: forms.py:123 msgid "The email address and/or password you specified are not correct." -msgstr "Email-адрес или пароль некорректны." +msgstr "Email-адрес и/или пароль введено некорректно." #: forms.py:138 msgid "Current Password" -msgstr "Действующий пароль" +msgstr "Текущий пароль" #: forms.py:142 forms.py:180 msgid "New Password" -msgstr "Новый Пароль" +msgstr "Новый пароль" #: forms.py:146 forms.py:184 msgid "New Password (again)" -msgstr "Новый Пароль (еще раз)" +msgstr "Новый пароль (еще раз)" #: forms.py:156 msgid "Please type your current password." -msgstr "Пожалуйста, введите ваш действующий пароль." +msgstr "Пожалуйста, введите ваш текущий пароль." #: forms.py:173 msgid "Email address can not be found." -msgstr "Email-адрес не найден" +msgstr "Указанный адрес электронной почты не найден." #: forms.py:199 msgid "Timezone" @@ -126,17 +129,17 @@ msgstr "подтверждения email" #: views.py:42 #, python-brace-format msgid "Confirmation email sent to {email}." -msgstr "Письмо с подтверждением email было отправлено по адресу {email}" +msgstr "Письмо с подтверждением было отправлено на {email}." #: views.py:46 #, python-brace-format msgid "The code {code} is invalid." -msgstr "Код {code} неверный" +msgstr "Код {code} - неверный." #: views.py:379 #, python-brace-format msgid "You have confirmed {email}." -msgstr "Вы подтвердили email с адресом {email}" +msgstr "Вы успешно подтвердили {email}." #: views.py:452 views.py:585 msgid "Password successfully changed." @@ -152,5 +155,5 @@ msgid "" "Your account is now inactive and your data will be expunged in the next " "{expunge_hours} hours." msgstr "" -"Ваш аккаунт теперь не активен и ваши данные будут удалены в следующие " +"Ваш аккаунт сейчас неактивен. Вся информация о нём будет удалена в течение " "{expunge_hours} часов." From 02debe2bd081f0bc88265a236fe54d06bc1fd350 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:21:21 -0600 Subject: [PATCH 181/239] Try out new composite actions --- .github/workflows/ci.yaml | 42 ++++----------------------------------- tox.ini | 2 +- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 897f2293..37aa165d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,24 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.9 - - - name: isort - uses: jamescurtin/isort-action@master - - - name: Lints - uses: py-actions/flake8@v1 - with: - ignore: "E265,E501" - max-line-length: "100" - exclude: "account/migrations,docs" - path: account - + - uses: pinax/linting@v2 test: name: Testing @@ -34,24 +17,7 @@ jobs: django: [2.2.*, 3.2.*] steps: - - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v1 + - uses: pinax/testing@v1 with: - python-version: ${{ matrix.python }} - - - name: Install Django and Testing Tools - run: | - pip install Django==${{ matrix.django }} coverage pytest - pip install . - - - name: Running Python Tests - run: coverage run runtests.py - - - name: Upload Coverage Report - if: matrix.django == '3.2.*' && matrix.python == '3.10' - run: | - pip install codecov - coverage xml - codecov --required -X search gcov pycov -f coverage.xml + python: ${{ matrix.python }} + django: ${{ matrix.django }} diff --git a/tox.ini b/tox.ini index 4d287a9b..1325cd92 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ ignore = E265,E501 max-line-length = 100 max-complexity = 10 -exclude = account/migrations,docs +exclude = **/migrations,docs inline-quotes = double [coverage:run] From d214408d11898e4eeef73ec35d1e085fca5b6244 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:23:50 -0600 Subject: [PATCH 182/239] Update to new version --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 37aa165d..64ad25bd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: django: [2.2.*, 3.2.*] steps: - - uses: pinax/testing@v1 + - uses: pinax/testing@v2 with: python: ${{ matrix.python }} django: ${{ matrix.django }} From bb8be6c25cdc4297e180fd05813e12032a00ee98 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:25:30 -0600 Subject: [PATCH 183/239] Make a flake error --- account/decorators.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index 82a8658b..900bd16c 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -1,10 +1,7 @@ import functools - -from django.contrib.auth import REDIRECT_FIELD_NAME - from account.utils import handle_redirect_to_login - +from django.contrib.auth import REDIRECT_FIELD_NAME def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): """ Decorator for views that checks that the user is logged in, redirecting From e028bfa56079f7a58eb61ace981b6274bcf5da07 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:26:26 -0600 Subject: [PATCH 184/239] Revert "Make a flake error" This reverts commit bb8be6c25cdc4297e180fd05813e12032a00ee98. --- account/decorators.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/account/decorators.py b/account/decorators.py index 900bd16c..82a8658b 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -1,7 +1,10 @@ import functools -from account.utils import handle_redirect_to_login from django.contrib.auth import REDIRECT_FIELD_NAME + +from account.utils import handle_redirect_to_login + + def login_required(func=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None): """ Decorator for views that checks that the user is logged in, redirecting From 2f9f8ca89d7cf7802fde11338187a1711dc8343a Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:50:26 -0600 Subject: [PATCH 185/239] Build out change log --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd58f788..2f14656d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## Next + +* #205 - Bug fix on checking email against email not signup code +* #225 - Fix case sensitivity mismatch on email addresses +* #233 - Fix link to languages in docs +* #247 - Update Spanish translations +* #273 - Update German translations +* #135 - Update Russian translations +* #242 - Fix callbacks/hooks for account deletion +* #251 (#249) - Allow overriding the password reset token url +* #280 - Raise improper config error if signup view can't login +* #348 (#337) - Make https the default protocol +* #351 (#332) - Reduction in queries +* #360 (#210) - Updates to docs +* #361 (#141) - Added ability to override clean passwords +* #362 - Updated CI to use Pinax Actions +* Updates to packaging +* Dropped Python 3.5 and Django 3.1 from test matrix +* Added Python 3.10 to test matrix + + ## 3.0.3 * Fix deprecated urls From 8ec8c26b7ba3a4586db11c525263b765eedb706d Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 16:52:54 -0600 Subject: [PATCH 186/239] Update README --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a9182354..e25d9cfb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![](https://img.shields.io/pypi/v/django-user-accounts.svg)](https://pypi.python.org/pypi/django-user-accounts/) -[![CircleCi](https://img.shields.io/circleci/project/github/pinax/django-user-accounts.svg)](https://circleci.com/gh/pinax/django-user-accounts) +[![Build](https://github.com/pinax/django-user-accounts/actions/workflows/ci.yaml/badge.svg)](https://github.com/pinax/django-user-accounts/actions) [![Codecov](https://img.shields.io/codecov/c/github/pinax/django-user-accounts.svg)](https://codecov.io/gh/pinax/django-user-accounts) [![](https://img.shields.io/github/contributors/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/graphs/contributors) [![](https://img.shields.io/github/issues-pr/pinax/django-user-accounts.svg)](https://github.com/pinax/django-user-accounts/pulls) @@ -54,17 +54,16 @@ Pinax is an open-source platform built on the Django Web Framework. It is an eco #### Supported Django and Python versions -Django / Python | 3.6 | 3.7 | 3.8 | 3.9 ---------------- | --- | --- | --- | --- -2.2 | * | * | * | * -3.0 | * | * | * | * -3.1 | * | * | * | * +Django / Python | 3.6 | 3.7 | 3.8 | 3.9 | 3.10 +--------------- | --- | --- | --- | --- | ---- +2.2 | * | * | * | * | * +3.2 | * | * | * | * | * ## Requirements -* Django 2.2, 3.0, or 3.1 -* Python 3.6, 3.7, 3.8, or 3.9 +* Django 2.2 or 3.2 +* Python 3.6, 3.7, 3.8, 3.9, 3.10 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) From 6d5c546762802f83be9af260b19ca9a10fe2af4e Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 19:21:49 -0600 Subject: [PATCH 187/239] Fix package --- setup.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9eaabe46..adb34b41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ author_email = team@pinaxproject.com description = a Django user account app long_description = file: README.md long_description_content_type = text/markdown -license = MIT, +license = MIT url = http://github.com/pinax/django-user-accounts classifiers = Development Status :: 5 - Production/Stable @@ -31,7 +31,7 @@ classifiers = [options] package_dir = - = account + = . packages = find: include_package_data = True install_requires = @@ -41,7 +41,7 @@ install_requires = zip_safe = False [options.packages.find] -where = account +where = . [options.package_data] account = locale/*/LC_MESSAGES/* From e83effdd4a23cd8d830169904c261ff6677ee3e6 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Wed, 24 Nov 2021 19:36:32 -0600 Subject: [PATCH 188/239] 3.1.0 --- CHANGELOG.md | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f14656d..15d18518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. -## Next +## 3.1.0 * #205 - Bug fix on checking email against email not signup code * #225 - Fix case sensitivity mismatch on email addresses diff --git a/setup.cfg b/setup.cfg index adb34b41..5d704cc9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ DJANGO_SETTINGS_MODULE = account.tests.settings [metadata] name = django-user-accounts -version = 4.0.0 +version = 3.1.0 author = Pinax Team author_email = team@pinaxproject.com description = a Django user account app From 872fb1d98d40ef2312a6c63e58cbb93de434a0a4 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Fri, 26 Nov 2021 13:03:54 -0800 Subject: [PATCH 189/239] Remove unnecessary max_length Based on feedback --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index aed81d57..3c9cc996 100644 --- a/account/models.py +++ b/account/models.py @@ -132,7 +132,7 @@ class InvalidCode(Exception): max_uses = models.PositiveIntegerField(default=1) expiry = models.DateTimeField(null=True, blank=True) inviter = models.ForeignKey(AUTH_USER_MODEL, null=True, blank=True) - email = models.EmailField(blank=True, max_length=254) + email = models.EmailField(blank=True) notes = models.TextField(blank=True) sent = models.DateTimeField(null=True, blank=True) created = models.DateTimeField(default=timezone.now, editable=False) From 912b37390ed17bcf298f6ac2a0424a1b24ec735f Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Fri, 26 Nov 2021 13:04:46 -0800 Subject: [PATCH 190/239] Remove the other max_length for email field --- account/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account/models.py b/account/models.py index 3c9cc996..ab7f8cca 100644 --- a/account/models.py +++ b/account/models.py @@ -34,7 +34,7 @@ class Account(models.Model): user = models.OneToOneField(AUTH_USER_MODEL, related_name="account", verbose_name=_("user")) timezone = TimeZoneField(_("timezone")) language = models.CharField(_("language"), - max_length=10, + =25=10, choices=settings.ACCOUNT_LANGUAGES, default=settings.LANGUAGE_CODE ) @@ -235,7 +235,7 @@ def save(self, **kwargs): class EmailAddress(models.Model): user = models.ForeignKey(AUTH_USER_MODEL) - email = models.EmailField(unique=settings.ACCOUNT_EMAIL_UNIQUE, max_length=254) + email = models.EmailField(unique=settings.ACCOUNT_EMAIL_UNIQUE) verified = models.BooleanField(default=False) primary = models.BooleanField(default=False) @@ -341,7 +341,7 @@ def send(self, **kwargs): class AccountDeletion(models.Model): user = models.ForeignKey(AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL) - email = models.EmailField(max_length=254) + email = models.EmailField() date_requested = models.DateTimeField(default=timezone.now) date_expunged = models.DateTimeField(null=True, blank=True) From c79a689bd97e9652345029c42ce20f3dfc0534f9 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Fri, 26 Nov 2021 13:05:21 -0800 Subject: [PATCH 191/239] fix typo in last commit --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index ab7f8cca..218c8dd2 100644 --- a/account/models.py +++ b/account/models.py @@ -34,7 +34,7 @@ class Account(models.Model): user = models.OneToOneField(AUTH_USER_MODEL, related_name="account", verbose_name=_("user")) timezone = TimeZoneField(_("timezone")) language = models.CharField(_("language"), - =25=10, + max_length=10, choices=settings.ACCOUNT_LANGUAGES, default=settings.LANGUAGE_CODE ) From e143184a667840e8f2e971ccf6d92af808cc99b4 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 14 Jan 2022 14:40:20 -0800 Subject: [PATCH 192/239] ugettext_lazy -> gettext_lazy for Django 4.0 --- account/hooks.py | 2 +- docs/usage.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/account/hooks.py b/account/hooks.py index 8cf2d1cf..3dc70943 100644 --- a/account/hooks.py +++ b/account/hooks.py @@ -4,7 +4,7 @@ from django import forms from django.core.mail import send_mail from django.template.loader import render_to_string -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from account.conf import settings diff --git a/docs/usage.rst b/docs/usage.rst index 9d77ab22..d22faa61 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -298,7 +298,7 @@ checker from `cracklib`_.:: from django import forms from django.conf import settings from django.template.defaultfilters import mark_safe from - django.utils.translation import ugettext_lazy as _ + django.utils.translation import gettext_lazy as _ from account.hooks import AccountDefaultHookSet From 1be38b0d6054649b8e7850922138013ac631992a Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 14 Jan 2022 16:26:01 -0800 Subject: [PATCH 193/239] Bump version to 3.2.0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5d704cc9..6767e050 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ DJANGO_SETTINGS_MODULE = account.tests.settings [metadata] name = django-user-accounts -version = 3.1.0 +version = 3.2.0 author = Pinax Team author_email = team@pinaxproject.com description = a Django user account app From 3db0f4f339ef9b466afccd611640c765be2ccdb4 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 14 Jan 2022 16:26:23 -0800 Subject: [PATCH 194/239] Add changes to changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d18518..e95b0e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 3.2.0 + +* #363 - Django 4.0 compat: `ugettext_lazy` -> `gettext_lazy` + ## 3.1.0 * #205 - Bug fix on checking email against email not signup code From 24e2dce56dc4ade3cc1c27725b82cde68b548899 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 14 Jan 2022 23:28:34 -0800 Subject: [PATCH 195/239] Replace removed Request.is_ajax() with our own utility --- account/utils.py | 8 ++++++++ account/views.py | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/account/utils.py b/account/utils.py index 4163170b..aba7d390 100644 --- a/account/utils.py +++ b/account/utils.py @@ -107,6 +107,14 @@ def get_form_data(form, field_name, default=None): return form.data.get(key, default) +# https://stackoverflow.com/a/70419609/6461688 +def is_ajax(request): + """ + Return True if the request was sent with XMLHttpRequest, False otherwise. + """ + return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + + def check_password_expired(user): """ Return True if password is expired and system is using diff --git a/account/views.py b/account/views.py index 3c237f43..f1237983 100644 --- a/account/views.py +++ b/account/views.py @@ -36,7 +36,7 @@ PasswordHistory, SignupCode, ) -from account.utils import default_redirect, get_form_data +from account.utils import default_redirect, get_form_data, is_ajax class PasswordMixin(object): @@ -195,7 +195,7 @@ def get_initial(self): return initial def get_template_names(self): - if self.request.is_ajax(): + if is_ajax(request): return [self.template_name_ajax] else: return [self.template_name] @@ -327,7 +327,7 @@ def is_open(self): return settings.ACCOUNT_OPEN_SIGNUP def email_confirmation_required_response(self): - if self.request.is_ajax(): + if is_ajax(self.request): template_name = self.template_name_email_confirmation_sent_ajax else: template_name = self.template_name_email_confirmation_sent @@ -342,7 +342,7 @@ def email_confirmation_required_response(self): return self.response_class(**response_kwargs) def closed(self): - if self.request.is_ajax(): + if is_ajax(self.request): template_name = self.template_name_signup_closed_ajax else: template_name = self.template_name_signup_closed @@ -373,7 +373,7 @@ def get(self, *args, **kwargs): return super(LoginView, self).get(*args, **kwargs) def get_template_names(self): - if self.request.is_ajax(): + if is_ajax(self.request): return [self.template_name_ajax] else: return [self.template_name] From f6031254d039b65191676ab34f6f8b162233be29 Mon Sep 17 00:00:00 2001 From: Patrick Altman Date: Sat, 22 Jan 2022 16:35:57 -0600 Subject: [PATCH 196/239] Fix typo --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index f1237983..8a591307 100644 --- a/account/views.py +++ b/account/views.py @@ -195,7 +195,7 @@ def get_initial(self): return initial def get_template_names(self): - if is_ajax(request): + if is_ajax(self.request): return [self.template_name_ajax] else: return [self.template_name] From 4cf89a0c75fda478dc49ccbb2e0a0140705fc0b9 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 18 Jan 2022 02:10:07 -0800 Subject: [PATCH 197/239] Use QuerySet.select_related() on specific Admin pages to avoid n+1 queries --- account/admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/account/admin.py b/account/admin.py index 20ae94ee..c2f60b5f 100644 --- a/account/admin.py +++ b/account/admin.py @@ -22,6 +22,9 @@ class AccountAdmin(admin.ModelAdmin): raw_id_fields = ["user"] + def get_queryset(self, request): + return super().get_queryset(request).select_related('user') + class AccountDeletionAdmin(AccountAdmin): @@ -33,6 +36,9 @@ class EmailAddressAdmin(AccountAdmin): list_display = ["user", "email", "verified", "primary"] search_fields = ["email", "user__username"] + def get_queryset(self, request): + return super().get_queryset(request).select_related('user') + class PasswordExpiryAdmin(admin.ModelAdmin): @@ -46,6 +52,9 @@ class PasswordHistoryAdmin(admin.ModelAdmin): list_filter = ["user"] ordering = ["user__username", "-timestamp"] + def get_queryset(self, request): + return super().get_queryset(request).select_related('user') + admin.site.register(Account, AccountAdmin) admin.site.register(SignupCode, SignupCodeAdmin) From 50a292bda44b88e21b9c87a42ef4d3b400d76e0b Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 18 Jan 2022 02:21:48 -0800 Subject: [PATCH 198/239] Add performance fix to the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e95b0e36..ea8967ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ version with these. Your code will need to be updated to continue working. ## 3.2.0 * #363 - Django 4.0 compat: `ugettext_lazy` -> `gettext_lazy` +* #364 - Performance fix to admin classes ## 3.1.0 From 863126117e4458534ccbfd9bb10a100d8d6c9cc3 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 25 Jan 2022 22:53:38 -0800 Subject: [PATCH 199/239] Update changelog and bump version to 3.2.1 --- CHANGELOG.md | 5 ++++- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea8967ea..d263ac8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,13 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 3.2.1 + +* #364 - Performance fix to admin classes + ## 3.2.0 * #363 - Django 4.0 compat: `ugettext_lazy` -> `gettext_lazy` -* #364 - Performance fix to admin classes ## 3.1.0 diff --git a/setup.cfg b/setup.cfg index 6767e050..ce16d1ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ DJANGO_SETTINGS_MODULE = account.tests.settings [metadata] name = django-user-accounts -version = 3.2.0 +version = 3.2.1 author = Pinax Team author_email = team@pinaxproject.com description = a Django user account app From 68b8de85775d402883dce594a7b7782cd9d2e2ae Mon Sep 17 00:00:00 2001 From: Guenther Meyer Date: Wed, 2 Nov 2022 23:31:15 +0100 Subject: [PATCH 200/239] compiled german translation Signed-off-by: Guenther Meyer --- account/locale/de/LC_MESSAGES/django.mo | Bin 3962 -> 3922 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/account/locale/de/LC_MESSAGES/django.mo b/account/locale/de/LC_MESSAGES/django.mo index 880d129adcb3aae68e3432d253add5717570612a..4a033999631c56a09de9dd5a90052680a5af6829 100644 GIT binary patch delta 433 zcmXZYze@sf7{~F?Wp-XyY8oM0N05_S3Juv(NNWE<1R)JU`_U9!(AebQ(CB{<-2D$p z4N*C?G)O~#K$}zVhYpVK>&x?fcz)bGcn=;ECmJ}?TTVKZ)LfRfF~la0a2+R@#V_2! zZ*DI7|i+GAQ_&JiI%Y5{|L6`>au)xC*OE|_dCb)zXw1Hkz zKhgfo3@!f$t??YKkxwyeTt;?O1#OW!TAf`BcwF|;5p*RTY;CpEx&F7H|~HNGaLDA>79tp5Y`maTuS`!j>t4lnH)0Sj19FWDGBH1#eLo z{6Zf2KVzWI&!BERj=GVLy73IMqs*dSWEFLtjU0hNu#Gx#FSi1YX{CkrCu%Co^tb!s~k6PyM?0fPAgBTFl#hA!fP{({{yjsG=u;E From ef889ed1e9a7034d182429725edc627a8d5e08b0 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Thu, 9 Feb 2023 08:54:41 +0000 Subject: [PATCH 201/239] Update account/forms.py Co-authored-by: blag --- account/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/forms.py b/account/forms.py index 1b0c3714..ec081fbd 100644 --- a/account/forms.py +++ b/account/forms.py @@ -15,7 +15,7 @@ alnum_re = re.compile(r"^\w+$") User = get_user_model() -USER_FIELD_MAX_LENGTH = User._meta.get_field(User.USERNAME_FIELD).max_length +USER_FIELD_MAX_LENGTH = getattr(User, User.USERNAME_FIELD).field.max_length class PasswordField(forms.CharField): From fd8ead10fc401e567ed590f319cd3659830c81b9 Mon Sep 17 00:00:00 2001 From: Christopher Broderick Date: Thu, 9 Feb 2023 09:05:32 +0000 Subject: [PATCH 202/239] Fix redefining variable from outer scope --- account/forms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/account/forms.py b/account/forms.py index 2e927977..f403f5ee 100644 --- a/account/forms.py +++ b/account/forms.py @@ -63,7 +63,6 @@ class SignupForm(forms.Form): def clean_username(self): if not alnum_re.search(self.cleaned_data["username"]): raise forms.ValidationError(_("Usernames can only contain letters, numbers and the following special characters ./+/-/_")) - User = get_user_model() lookup_kwargs = get_user_lookup_kwargs({ "{username}__iexact": self.cleaned_data["username"] }) From 29f9e66a258b9af7a15394c1ce7f887582ee1d4c Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 02:27:07 -0600 Subject: [PATCH 203/239] Update GHA config --- .github/workflows/ci.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 64ad25bd..05dd5604 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,8 +13,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [3.6, 3.7, 3.8, 3.9, "3.10"] - django: [2.2.*, 3.2.*] + python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + django: + - "2.2.*" + - "3.2.*" + - "4.2.*" steps: - uses: pinax/testing@v2 From 92145a9d06158c7d2c56d83045c65c74859e544f Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 09:35:18 -0600 Subject: [PATCH 204/239] Move account/tests to ./tests --- account/tests/urls.py | 5 ----- makemigrations.py | 4 ++-- manage.py | 10 ++++++++++ runtests.py | 4 ++-- setup.cfg | 4 ++-- {account/tests => tests}/__init__.py | 0 {account/tests => tests}/settings.py | 4 ++-- .../account/email/email_confirmation_message.txt | 0 .../account/email/email_confirmation_subject.txt | 0 .../templates/account/email/password_change.txt | 0 .../account/email/password_change_subject.txt | 0 .../templates/account/email/password_reset.txt | 0 .../account/email/password_reset_subject.txt | 0 .../templates/account/email_confirm.html | 0 .../templates/account/email_confirmation_sent.html | 0 .../tests => tests}/templates/account/login.html | 0 .../tests => tests}/templates/account/logout.html | 0 .../templates/account/password_change.html | 0 .../templates/account/password_reset_sent.html | 0 .../templates/account/password_reset_token.html | 0 .../account/password_reset_token_fail.html | 0 .../templates/account/settings.html | 0 .../tests => tests}/templates/account/signup.html | 0 .../templates/account/signup_closed.html | 0 {account/tests => tests}/test_auth.py | 0 {account/tests => tests}/test_commands.py | 4 ++-- {account/tests => tests}/test_decorators.py | 0 {account/tests => tests}/test_email_address.py | 0 {account/tests => tests}/test_models.py | 0 {account/tests => tests}/test_password.py | 14 +++++++------- {account/tests => tests}/test_views.py | 0 tests/urls.py | 9 +++++++++ tox.ini | 4 ++-- 33 files changed, 38 insertions(+), 24 deletions(-) delete mode 100644 account/tests/urls.py create mode 100644 manage.py rename {account/tests => tests}/__init__.py (100%) rename {account/tests => tests}/settings.py (96%) rename {account/tests => tests}/templates/account/email/email_confirmation_message.txt (100%) rename {account/tests => tests}/templates/account/email/email_confirmation_subject.txt (100%) rename {account/tests => tests}/templates/account/email/password_change.txt (100%) rename {account/tests => tests}/templates/account/email/password_change_subject.txt (100%) rename {account/tests => tests}/templates/account/email/password_reset.txt (100%) rename {account/tests => tests}/templates/account/email/password_reset_subject.txt (100%) rename {account/tests => tests}/templates/account/email_confirm.html (100%) rename {account/tests => tests}/templates/account/email_confirmation_sent.html (100%) rename {account/tests => tests}/templates/account/login.html (100%) rename {account/tests => tests}/templates/account/logout.html (100%) rename {account/tests => tests}/templates/account/password_change.html (100%) rename {account/tests => tests}/templates/account/password_reset_sent.html (100%) rename {account/tests => tests}/templates/account/password_reset_token.html (100%) rename {account/tests => tests}/templates/account/password_reset_token_fail.html (100%) rename {account/tests => tests}/templates/account/settings.html (100%) rename {account/tests => tests}/templates/account/signup.html (100%) rename {account/tests => tests}/templates/account/signup_closed.html (100%) rename {account/tests => tests}/test_auth.py (100%) rename {account/tests => tests}/test_commands.py (98%) rename {account/tests => tests}/test_decorators.py (100%) rename {account/tests => tests}/test_email_address.py (100%) rename {account/tests => tests}/test_models.py (100%) rename {account/tests => tests}/test_password.py (94%) rename {account/tests => tests}/test_views.py (100%) create mode 100644 tests/urls.py diff --git a/account/tests/urls.py b/account/tests/urls.py deleted file mode 100644 index a2c9a2f4..00000000 --- a/account/tests/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.conf.urls import include, url - -urlpatterns = [ - url(r"^", include("account.urls")), -] diff --git a/makemigrations.py b/makemigrations.py index eb2aa60d..894b4d75 100644 --- a/makemigrations.py +++ b/makemigrations.py @@ -11,7 +11,7 @@ "django.contrib.contenttypes", "django.contrib.sites", "account", - "account.tests" + "tests" ], MIDDLEWARE_CLASSES=[], DATABASES={ @@ -21,7 +21,7 @@ } }, SITE_ID=1, - ROOT_URLCONF="account.tests.urls", + ROOT_URLCONF="tests.urls", SECRET_KEY="notasecret", ) diff --git a/manage.py b/manage.py new file mode 100644 index 00000000..dc935d61 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/runtests.py b/runtests.py index 9b7c02de..1eb34476 100644 --- a/runtests.py +++ b/runtests.py @@ -6,7 +6,7 @@ def runtests(*test_args): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") django.setup() parent = os.path.dirname(os.path.abspath(__file__)) @@ -15,7 +15,7 @@ def runtests(*test_args): from django.test.runner import DiscoverRunner runner_class = DiscoverRunner if not test_args: - test_args = ["account.tests"] + test_args = ["tests"] failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) sys.exit(failures) diff --git a/setup.cfg b/setup.cfg index ce16d1ab..0e206cbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [tool:pytest] -testpaths = account/tests -DJANGO_SETTINGS_MODULE = account.tests.settings +testpaths = tests +DJANGO_SETTINGS_MODULE = tests.settings [metadata] name = django-user-accounts diff --git a/account/tests/__init__.py b/tests/__init__.py similarity index 100% rename from account/tests/__init__.py rename to tests/__init__.py diff --git a/account/tests/settings.py b/tests/settings.py similarity index 96% rename from account/tests/settings.py rename to tests/settings.py index 6ec3bbf8..386ad249 100644 --- a/account/tests/settings.py +++ b/tests/settings.py @@ -7,7 +7,7 @@ "django.contrib.sites", "django.contrib.messages", "account", - "account.tests", + "tests", ] DATABASES = { "default": { @@ -16,7 +16,7 @@ } } SITE_ID = 1 -ROOT_URLCONF = "account.tests.urls" +ROOT_URLCONF = "tests.urls" SECRET_KEY = "notasecret" TEMPLATES = [ { diff --git a/account/tests/templates/account/email/email_confirmation_message.txt b/tests/templates/account/email/email_confirmation_message.txt similarity index 100% rename from account/tests/templates/account/email/email_confirmation_message.txt rename to tests/templates/account/email/email_confirmation_message.txt diff --git a/account/tests/templates/account/email/email_confirmation_subject.txt b/tests/templates/account/email/email_confirmation_subject.txt similarity index 100% rename from account/tests/templates/account/email/email_confirmation_subject.txt rename to tests/templates/account/email/email_confirmation_subject.txt diff --git a/account/tests/templates/account/email/password_change.txt b/tests/templates/account/email/password_change.txt similarity index 100% rename from account/tests/templates/account/email/password_change.txt rename to tests/templates/account/email/password_change.txt diff --git a/account/tests/templates/account/email/password_change_subject.txt b/tests/templates/account/email/password_change_subject.txt similarity index 100% rename from account/tests/templates/account/email/password_change_subject.txt rename to tests/templates/account/email/password_change_subject.txt diff --git a/account/tests/templates/account/email/password_reset.txt b/tests/templates/account/email/password_reset.txt similarity index 100% rename from account/tests/templates/account/email/password_reset.txt rename to tests/templates/account/email/password_reset.txt diff --git a/account/tests/templates/account/email/password_reset_subject.txt b/tests/templates/account/email/password_reset_subject.txt similarity index 100% rename from account/tests/templates/account/email/password_reset_subject.txt rename to tests/templates/account/email/password_reset_subject.txt diff --git a/account/tests/templates/account/email_confirm.html b/tests/templates/account/email_confirm.html similarity index 100% rename from account/tests/templates/account/email_confirm.html rename to tests/templates/account/email_confirm.html diff --git a/account/tests/templates/account/email_confirmation_sent.html b/tests/templates/account/email_confirmation_sent.html similarity index 100% rename from account/tests/templates/account/email_confirmation_sent.html rename to tests/templates/account/email_confirmation_sent.html diff --git a/account/tests/templates/account/login.html b/tests/templates/account/login.html similarity index 100% rename from account/tests/templates/account/login.html rename to tests/templates/account/login.html diff --git a/account/tests/templates/account/logout.html b/tests/templates/account/logout.html similarity index 100% rename from account/tests/templates/account/logout.html rename to tests/templates/account/logout.html diff --git a/account/tests/templates/account/password_change.html b/tests/templates/account/password_change.html similarity index 100% rename from account/tests/templates/account/password_change.html rename to tests/templates/account/password_change.html diff --git a/account/tests/templates/account/password_reset_sent.html b/tests/templates/account/password_reset_sent.html similarity index 100% rename from account/tests/templates/account/password_reset_sent.html rename to tests/templates/account/password_reset_sent.html diff --git a/account/tests/templates/account/password_reset_token.html b/tests/templates/account/password_reset_token.html similarity index 100% rename from account/tests/templates/account/password_reset_token.html rename to tests/templates/account/password_reset_token.html diff --git a/account/tests/templates/account/password_reset_token_fail.html b/tests/templates/account/password_reset_token_fail.html similarity index 100% rename from account/tests/templates/account/password_reset_token_fail.html rename to tests/templates/account/password_reset_token_fail.html diff --git a/account/tests/templates/account/settings.html b/tests/templates/account/settings.html similarity index 100% rename from account/tests/templates/account/settings.html rename to tests/templates/account/settings.html diff --git a/account/tests/templates/account/signup.html b/tests/templates/account/signup.html similarity index 100% rename from account/tests/templates/account/signup.html rename to tests/templates/account/signup.html diff --git a/account/tests/templates/account/signup_closed.html b/tests/templates/account/signup_closed.html similarity index 100% rename from account/tests/templates/account/signup_closed.html rename to tests/templates/account/signup_closed.html diff --git a/account/tests/test_auth.py b/tests/test_auth.py similarity index 100% rename from account/tests/test_auth.py rename to tests/test_auth.py diff --git a/account/tests/test_commands.py b/tests/test_commands.py similarity index 98% rename from account/tests/test_commands.py rename to tests/test_commands.py index 4a734dd5..d9ce07a7 100644 --- a/account/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,8 +4,8 @@ from django.core.management import call_command from django.test import TestCase, override_settings -from ..conf import settings -from ..models import PasswordExpiry, PasswordHistory +from account.conf import settings +from account.models import PasswordExpiry, PasswordHistory @override_settings( diff --git a/account/tests/test_decorators.py b/tests/test_decorators.py similarity index 100% rename from account/tests/test_decorators.py rename to tests/test_decorators.py diff --git a/account/tests/test_email_address.py b/tests/test_email_address.py similarity index 100% rename from account/tests/test_email_address.py rename to tests/test_email_address.py diff --git a/account/tests/test_models.py b/tests/test_models.py similarity index 100% rename from account/tests/test_models.py rename to tests/test_models.py diff --git a/account/tests/test_password.py b/tests/test_password.py similarity index 94% rename from account/tests/test_password.py rename to tests/test_password.py index 0b2f23b5..399368d2 100644 --- a/account/tests/test_password.py +++ b/tests/test_password.py @@ -8,8 +8,8 @@ import pytz -from ..models import PasswordExpiry, PasswordHistory -from ..utils import check_password_expired +from account.models import PasswordExpiry, PasswordHistory +from account.utils import check_password_expired def middleware_kwarg(value): @@ -83,7 +83,7 @@ def test_get_not_expired(self): # get account settings page (could be any application page) response = self.client.get(reverse("account_settings")) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_get_expired(self): """ @@ -125,7 +125,7 @@ def test_password_expiration_reset(self): post_data ) # Should see one more history entry for this user - self.assertEquals(self.user.password_history.count(), history_count + 1) + self.assertEqual(self.user.password_history.count(), history_count + 1) latest = PasswordHistory.objects.latest("timestamp") self.assertTrue(latest != self.history) @@ -164,7 +164,7 @@ def test_get_no_history(self): ): # get account settings page (could be any application page) response = self.client.get(reverse("account_settings")) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_password_expiration_reset(self): """ @@ -191,7 +191,7 @@ def test_password_expiration_reset(self): post_data ) # Should see one more history entry for this user - self.assertEquals(self.user.password_history.count(), history_count + 1) + self.assertEqual(self.user.password_history.count(), history_count + 1) def test_password_reset(self): """ @@ -216,4 +216,4 @@ def test_password_reset(self): post_data ) # history count should be zero - self.assertEquals(self.user.password_history.count(), 0) + self.assertEqual(self.user.password_history.count(), 0) diff --git a/account/tests/test_views.py b/tests/test_views.py similarity index 100% rename from account/tests/test_views.py rename to tests/test_views.py diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 00000000..6f843f42 --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,9 @@ +import django +if django.VERSION >= (4, 0): + from django.urls import include, re_path as url +else: + from django.conf.urls import include, url + +urlpatterns = [ + url(r"^", include("account.urls")), +] diff --git a/tox.ini b/tox.ini index 1325cd92..65db9c11 100644 --- a/tox.ini +++ b/tox.ini @@ -7,12 +7,12 @@ inline-quotes = double [coverage:run] source = account -omit = account/conf.py,account/tests/*,account/migrations/* +omit = account/conf.py,tests/*,account/migrations/* branch = true data_file = .coverage [coverage:report] -omit = account/conf.py,account/tests/*,account/migrations/* +omit = account/conf.py,tests/*,account/migrations/* exclude_lines = coverage: omit show_missing = True From 2481bf9bbeffea87842b73d6296474b06c25b2f5 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 09:39:07 -0600 Subject: [PATCH 205/239] Convert setup.cfg to pyproject.toml --- pyproject.toml | 62 +++++++++++++++++++++++++++++++++++++++++++++----- setup.cfg | 47 -------------------------------------- 2 files changed, 56 insertions(+), 53 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index d98fb179..688adc2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,44 @@ +[build-system] +requires = ["setuptools>=61.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-user-accounts" +version = "3.2.1" +authors = [{name = "Pinax Team", email = "team@pinaxproject.com"}] +description = "a Django user account app" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dependencies = [ + "Django>=2.2", + "django-appconf>=1.0.4", + "pytz>=2020.4", +] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[project.urls] +Homepage = "http://github.com/pinax/django-user-accounts" + [tool.isort] profile = "hug" src_paths = ["account"] @@ -8,9 +49,18 @@ sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" skip_glob = "account/migrations/*,docs" include_trailing_comma = "True" -[build-system] -requires = [ - "setuptools>=42", - "wheel" -] -build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +testpaths = ["tests"] +DJANGO_SETTINGS_MODULE = "tests.settings" + +[tool.setuptools] +package-dir = {"" = "."} +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +where = ["."] +namespaces = false + +[tool.setuptools.package-data] +account = ["locale/*/LC_MESSAGES/*"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0e206cbc..00000000 --- a/setup.cfg +++ /dev/null @@ -1,47 +0,0 @@ -[tool:pytest] -testpaths = tests -DJANGO_SETTINGS_MODULE = tests.settings - -[metadata] -name = django-user-accounts -version = 3.2.1 -author = Pinax Team -author_email = team@pinaxproject.com -description = a Django user account app -long_description = file: README.md -long_description_content_type = text/markdown -license = MIT -url = http://github.com/pinax/django-user-accounts -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Django - Framework :: Django :: 2.2 - Framework :: Django :: 3.2 - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - -[options] -package_dir = - = . -packages = find: -include_package_data = True -install_requires = - Django>=2.2 - django-appconf>=1.0.4 - pytz>=2020.4 -zip_safe = False - -[options.packages.find] -where = . - -[options.package_data] -account = locale/*/LC_MESSAGES/* From e517cf89258e1ce1637d9f793794a23a7c87a4f0 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 09:39:58 -0600 Subject: [PATCH 206/239] Bugfix: Use timezone-aware now() in comparison --- account/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/account/utils.py b/account/utils.py index 30e7b269..c8330755 100644 --- a/account/utils.py +++ b/account/utils.py @@ -6,6 +6,7 @@ from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict from django.urls import NoReverseMatch, reverse +from django.utils import timezone from django.utils.encoding import force_str import pytz @@ -139,7 +140,7 @@ def check_password_expired(user): except PasswordHistory.DoesNotExist: return False - now = datetime.datetime.now() + now = timezone.now() expiration = latest.timestamp + datetime.timedelta(seconds=expiry) if expiration < now: From f5992fd77c489b9d6d5c1252f24bd7914ea4c01f Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 10:00:55 -0600 Subject: [PATCH 207/239] Simplify pyproject.toml and set version dynamically --- account/__init__.py | 4 +--- pyproject.toml | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/account/__init__.py b/account/__init__.py index 7d51e983..1da6a555 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1,3 +1 @@ -import pkg_resources - -__version__ = pkg_resources.get_distribution("django-user-accounts").version +__version__ = "3.2.1" diff --git a/pyproject.toml b/pyproject.toml index 688adc2e..91cea489 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta" [project] name = "django-user-accounts" -version = "3.2.1" authors = [{name = "Pinax Team", email = "team@pinaxproject.com"}] description = "a Django user account app" classifiers = [ @@ -31,6 +30,7 @@ dependencies = [ "django-appconf>=1.0.4", "pytz>=2020.4", ] +dynamic = ["version"] [project.readme] file = "README.md" @@ -54,13 +54,12 @@ testpaths = ["tests"] DJANGO_SETTINGS_MODULE = "tests.settings" [tool.setuptools] -package-dir = {"" = "."} +packages = ["account"] include-package-data = true zip-safe = false -[tool.setuptools.packages.find] -where = ["."] -namespaces = false +[tool.setuptools.dynamic] +version = {attr = "account.__version__"} [tool.setuptools.package-data] account = ["locale/*/LC_MESSAGES/*"] From 1e0903fb3b90de4fcd1539db88c7b14b1e84c285 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 10:16:26 -0600 Subject: [PATCH 208/239] Format with ruff --- account/forms.py | 4 +++- account/models.py | 14 ++++++++++++-- account/urls.py | 6 +++++- account/utils.py | 1 - account/views.py | 20 ++++++++++++++++---- pyproject.toml | 6 ++++++ tests/test_commands.py | 8 ++++++-- tests/test_password.py | 4 +++- tests/test_views.py | 4 +++- 9 files changed, 54 insertions(+), 13 deletions(-) diff --git a/account/forms.py b/account/forms.py index f403f5ee..e7786ceb 100644 --- a/account/forms.py +++ b/account/forms.py @@ -62,7 +62,9 @@ class SignupForm(forms.Form): def clean_username(self): if not alnum_re.search(self.cleaned_data["username"]): - raise forms.ValidationError(_("Usernames can only contain letters, numbers and the following special characters ./+/-/_")) + raise forms.ValidationError( + _("Usernames can only contain letters, numbers and the following special characters ./+/-/_") + ) lookup_kwargs = get_user_lookup_kwargs({ "{username}__iexact": self.cleaned_data["username"] }) diff --git a/account/models.py b/account/models.py index 9967a316..02fe4c8b 100644 --- a/account/models.py +++ b/account/models.py @@ -26,7 +26,12 @@ class Account(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="account", verbose_name=_("user"), on_delete=models.CASCADE) + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + related_name="account", + verbose_name=_("user"), + on_delete=models.CASCADE, + ) timezone = TimeZoneField(_("timezone")) language = models.CharField( _("language"), @@ -407,5 +412,10 @@ class PasswordExpiry(models.Model): """ Holds the password expiration period for a single user. """ - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="password_expiry", verbose_name=_("user"), on_delete=models.CASCADE) + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + related_name="password_expiry", + verbose_name=_("user"), + on_delete=models.CASCADE, + ) expiry = models.PositiveIntegerField(default=0) diff --git a/account/urls.py b/account/urls.py index f6eabecd..5fa6c46c 100644 --- a/account/urls.py +++ b/account/urls.py @@ -19,7 +19,11 @@ path("confirm_email//", ConfirmEmailView.as_view(), name="account_confirm_email"), path("password/", ChangePasswordView.as_view(), name="account_password"), path("password/reset/", PasswordResetView.as_view(), name="account_password_reset"), - path("password/reset///", PasswordResetTokenView.as_view(), name="account_password_reset_token"), + path( + "password/reset///", + PasswordResetTokenView.as_view(), + name="account_password_reset_token", + ), path("settings/", SettingsView.as_view(), name="account_settings"), path("delete/", DeleteView.as_view(), name="account_delete"), ] diff --git a/account/utils.py b/account/utils.py index c8330755..c9e67d78 100644 --- a/account/utils.py +++ b/account/utils.py @@ -9,7 +9,6 @@ from django.utils import timezone from django.utils.encoding import force_str -import pytz from account.conf import settings from .models import PasswordHistory diff --git a/account/views.py b/account/views.py index 8a591307..63242408 100644 --- a/account/views.py +++ b/account/views.py @@ -70,7 +70,10 @@ def get_context_data(self, **kwargs): redirect_field_name = self.get_redirect_field_name() ctx.update({ "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + "redirect_field_value": self.request.POST.get( + redirect_field_name, + self.request.GET.get(redirect_field_name, ""), + ), }) return ctx @@ -383,7 +386,10 @@ def get_context_data(self, **kwargs): redirect_field_name = self.get_redirect_field_name() ctx.update({ "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + "redirect_field_value": self.request.POST.get( + redirect_field_name, + self.request.GET.get(redirect_field_name, ""), + ), }) return ctx @@ -448,7 +454,10 @@ def get_context_data(self, **kwargs): redirect_field_name = self.get_redirect_field_name() ctx.update({ "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + "redirect_field_value": self.request.POST.get( + redirect_field_name, + self.request.GET.get(redirect_field_name, ""), + ), }) return ctx @@ -790,7 +799,10 @@ def get_context_data(self, **kwargs): redirect_field_name = self.get_redirect_field_name() ctx.update({ "redirect_field_name": redirect_field_name, - "redirect_field_value": self.request.POST.get(redirect_field_name, self.request.GET.get(redirect_field_name, "")), + "redirect_field_value": self.request.POST.get( + redirect_field_name, + self.request.GET.get(redirect_field_name, ""), + ), }) return ctx diff --git a/pyproject.toml b/pyproject.toml index 91cea489..1f037490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,12 @@ include_trailing_comma = "True" testpaths = ["tests"] DJANGO_SETTINGS_MODULE = "tests.settings" +[tool.ruff] +line-length = 120 + +[tool.ruff.per-file-ignores] +"account/migrations/**.py" = ["E501"] + [tool.setuptools] packages = ["account"] include-package-data = true diff --git a/tests/test_commands.py b/tests/test_commands.py index d9ce07a7..4747a61b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -34,7 +34,9 @@ def test_set_explicit_password_expiry(self): user = self.UserModel.objects.get(username="patrick") user_expiry = user.password_expiry self.assertEqual(user_expiry.expiry, expiration_period) - self.assertIn('User "{}" password expiration set to {} seconds'.format(self.user.username, expiration_period), out.getvalue()) + self.assertIn('User "{}" password expiration set to {} seconds'.format( + self.user.username, expiration_period), out.getvalue(), + ) def test_set_default_password_expiry(self): """ @@ -52,7 +54,9 @@ def test_set_default_password_expiry(self): user_expiry = user.password_expiry default_expiration = settings.ACCOUNT_PASSWORD_EXPIRY self.assertEqual(user_expiry.expiry, default_expiration) - self.assertIn('User "{}" password expiration set to {} seconds'.format(self.user.username, default_expiration), out.getvalue()) + self.assertIn('User "{}" password expiration set to {} seconds'.format( + self.user.username, default_expiration), out.getvalue(), + ) def test_reset_existing_password_expiry(self): """ diff --git a/tests/test_password.py b/tests/test_password.py index 399368d2..88f5f1ca 100644 --- a/tests/test_password.py +++ b/tests/test_password.py @@ -91,7 +91,9 @@ def test_get_expired(self): when retrieving account settings page if password is expired. """ # set PasswordHistory timestamp in past so password is expired. - self.history.timestamp = datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) + self.history.timestamp = ( + datetime.datetime.now(tz=pytz.UTC) - datetime.timedelta(days=1, seconds=self.expiry.expiry) + ) self.history.save() self.client.login(username=self.username, password=self.password) diff --git a/tests/test_views.py b/tests/test_views.py index 24343e37..0947cd8b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -257,7 +257,9 @@ def test_post_not_required(self): fetch_redirect_response=False ) - @override_settings(ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False, ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL="/somewhere/") + @override_settings( + ACCOUNT_EMAIL_CONFIRMATION_REQUIRED=False, ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL="/somewhere/" + ) def test_post_not_required_redirect_override(self): email_confirmation = self.signup() response = self.client.post(reverse("account_confirm_email", kwargs={"key": email_confirmation.key}), {}) From 14e7c34ff12dfd75252ae096fcaa2e4cf76ee8c2 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 13:23:01 -0600 Subject: [PATCH 209/239] Use re_path in tests app --- tests/urls.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/urls.py b/tests/urls.py index 6f843f42..4651b183 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,9 +1,5 @@ -import django -if django.VERSION >= (4, 0): - from django.urls import include, re_path as url -else: - from django.conf.urls import include, url +from django.urls import include, re_path urlpatterns = [ - url(r"^", include("account.urls")), + re_path(r"^", include("account.urls")), ] From 7b6d0bbd01d455faf59f1a9679d6c0bf16da8cb7 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 13:24:30 -0600 Subject: [PATCH 210/239] Don't package coverage config and test script --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index fe6c33ea..fc06f4b5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,7 @@ -include .coveragerc include CHANGELOG.md include LICENSE include tox.ini include README.md -include runtests.py recursive-include account *.html recursive-include account *.txt recursive-include account/locale * From 682716a047176f712823f634b07f6f1b50093fc7 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 13:27:02 -0600 Subject: [PATCH 211/239] Drop Django 2.2 --- README.md | 12 ++++++------ pyproject.toml | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e25d9cfb..7f400192 100644 --- a/README.md +++ b/README.md @@ -54,16 +54,16 @@ Pinax is an open-source platform built on the Django Web Framework. It is an eco #### Supported Django and Python versions -Django / Python | 3.6 | 3.7 | 3.8 | 3.9 | 3.10 ---------------- | --- | --- | --- | --- | ---- -2.2 | * | * | * | * | * -3.2 | * | * | * | * | * +Django / Python | 3.8 | 3.9 | 3.10 | 3.11 +--------------- | --- | --- | ---- | ---- + 3.2 | * | * | * | + 4.2 | * | * | * | * ## Requirements -* Django 2.2 or 3.2 -* Python 3.6, 3.7, 3.8, 3.9, 3.10 +* Django 3.2 or 4.2 +* Python 3.8, 3.9, 3.10, 3.11 * django-appconf (included in ``install_requires``) * pytz (included in ``install_requires``) diff --git a/pyproject.toml b/pyproject.toml index 1f037490..1cd4c310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django", - "Framework :: Django :: 2.2", "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", "Intended Audience :: Developers", @@ -18,15 +17,13 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] dependencies = [ - "Django>=2.2", + "Django>=3.2", "django-appconf>=1.0.4", "pytz>=2020.4", ] From f7a98dd94c20ec7d6f0626e695e2e0a571f3b308 Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 13:27:23 -0600 Subject: [PATCH 212/239] Update GHA config --- .github/workflows/ci.yaml | 40 ++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 05dd5604..d4900ace 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,7 +6,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: pinax/linting@v2 + - uses: actions/checkout@v2 + + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install lint dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Lint with ruff + run: | + ruff --format=github --target-version=py311 account tests test: name: Testing @@ -19,12 +33,28 @@ jobs: - "3.10" - "3.11" django: - - "2.2.*" - "3.2.*" - "4.2.*" + exclude: + - python: "3.11" + django: "3.2.*" steps: - - uses: pinax/testing@v2 + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v4 with: - python: ${{ matrix.python }} - django: ${{ matrix.django }} + python-version: ${{ matrix.python }} + + - name: Install Django + shell: bash + run: pip install Django==${{ matrix.django }} 'django-appconf>=1.0.4' 'pytz>=2020.4' + + - name: Install test utilities + shell: bash + run: pip install pytest pytest-django + + - name: Running Python Tests + shell: bash + run: pytest From f0ad1802119ab5bec2408aabef46feec59635e9e Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 14:41:29 -0600 Subject: [PATCH 213/239] Tweak DeepSource config --- .deepsource.toml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index b7578f0c..2783c4df 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,7 +1,15 @@ version = 1 +exclude_patterns = [ + "makemigrations.py", + "runtests.py", + "tests/**", +] + [[analyzers]] name = "python" enabled = true -runtime_version = "3.x.x" - + + [analyzers.meta] + max_line_length = 120 + runtime_version = "3.x.x" From a239c8f1a49dcebb441de8c7692bf67af6b62abc Mon Sep 17 00:00:00 2001 From: blag Date: Wed, 6 Sep 2023 14:41:53 -0600 Subject: [PATCH 214/239] DeepSource fixups --- account/views.py | 78 ++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/account/views.py b/account/views.py index 63242408..d448664a 100644 --- a/account/views.py +++ b/account/views.py @@ -200,8 +200,7 @@ def get_initial(self): def get_template_names(self): if is_ajax(self.request): return [self.template_name_ajax] - else: - return [self.template_name] + return [self.template_name] def get_form_kwargs(self): kwargs = super(SignupView, self).get_form_kwargs() @@ -235,25 +234,25 @@ def form_valid(self, form): self.send_email_confirmation(email_address) if settings.ACCOUNT_EMAIL_CONFIRMATION_REQUIRED and not email_address.verified: return self.email_confirmation_required_response() - else: - show_message = [ - settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL, - self.messages.get("email_confirmation_sent"), - not email_address.verified - ] - if all(show_message): - messages.add_message( - self.request, - self.messages["email_confirmation_sent"]["level"], - self.messages["email_confirmation_sent"]["text"].format(**{ - "email": form.cleaned_data["email"] - }) - ) - # attach form to self to maintain compatibility with login_user - # API. this should only be relied on by d-u-a and it is not a stable - # API for site developers. - self.form = form - self.login_user() + + show_message = [ + settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL, + self.messages.get("email_confirmation_sent"), + not email_address.verified + ] + if all(show_message): + messages.add_message( + self.request, + self.messages["email_confirmation_sent"]["level"], + self.messages["email_confirmation_sent"]["text"].format(**{ + "email": form.cleaned_data["email"] + }) + ) + # attach form to self to maintain compatibility with login_user + # API. this should only be relied on by d-u-a and it is not a stable + # API for site developers. + self.form = form # skipcq: PYL-W0201 + self.login_user() return redirect(self.get_success_url()) def create_user(self, form, commit=True, model=None, **kwargs): @@ -317,16 +316,16 @@ def get_code(self): def is_open(self): if self.signup_code: return True - else: - if self.signup_code_present: - if self.messages.get("invalid_signup_code"): - messages.add_message( - self.request, - self.messages["invalid_signup_code"]["level"], - self.messages["invalid_signup_code"]["text"].format(**{ - "code": self.get_code(), - }) - ) + + if self.signup_code_present and self.messages.get("invalid_signup_code"): + messages.add_message( + self.request, + self.messages["invalid_signup_code"]["level"], + self.messages["invalid_signup_code"]["text"].format(**{ + "code": self.get_code(), + }) + ) + return settings.ACCOUNT_OPEN_SIGNUP def email_confirmation_required_response(self): @@ -378,8 +377,7 @@ def get(self, *args, **kwargs): def get_template_names(self): if is_ajax(self.request): return [self.template_name_ajax] - else: - return [self.template_name] + return [self.template_name] def get_context_data(self, **kwargs): ctx = super(LoginView, self).get_context_data(**kwargs) @@ -550,8 +548,7 @@ def get_redirect_url(self): if not settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL: return settings.ACCOUNT_LOGIN_REDIRECT_URL return settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL - else: - return settings.ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL + return settings.ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL def after_confirmation(self, confirmation): user = confirmation.email_address.user @@ -598,9 +595,7 @@ def get_user(self): return self.request.user def get_form_kwargs(self): - """ - Returns the keyword arguments for instantiating the form. - """ + """Returns the keyword arguments for instantiating the form.""" kwargs = {"user": self.request.user, "initial": self.get_initial()} if self.request.method in ["POST", "PUT"]: kwargs.update({ @@ -650,11 +645,8 @@ def send_email(self, email): for user in User.objects.filter(pk__in=email_qs.values("user")): uid = int_to_base36(user.id) token = self.make_token(user) - password_reset_url = "{0}://{1}{2}".format( - protocol, - current_site.domain, - reverse(settings.ACCOUNT_PASSWORD_RESET_TOKEN_URL, kwargs=dict(uidb36=uid, token=token)) - ) + path = reverse(settings.ACCOUNT_PASSWORD_RESET_TOKEN_URL, kwargs=dict(uidb36=uid, token=token)) + password_reset_url = f"{protocol}://{current_site.domain}{path}" ctx = { "user": user, "current_site": current_site, From 15f4f2fc74bbaa2bd546687147507a25ec263938 Mon Sep 17 00:00:00 2001 From: blag Date: Thu, 7 Sep 2023 02:08:34 -0600 Subject: [PATCH 215/239] Exclude more things from our shipped distribution --- MANIFEST.in | 8 ++++++-- pyproject.toml | 5 ----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index fc06f4b5..372ea448 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,12 @@ include CHANGELOG.md include LICENSE -include tox.ini include README.md recursive-include account *.html recursive-include account *.txt recursive-include account/locale * -recursive-include docs Makefile conf.py *.rst +recursive-include docs *.rst +exclude tox.ini +recursive-exclude django_user_accounts.egg-info * +recursive-exclude tests * +exclude docs/conf.py +exclude docs/Makefile diff --git a/pyproject.toml b/pyproject.toml index 1cd4c310..cdb037a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,11 +58,6 @@ line-length = 120 [tool.setuptools] packages = ["account"] -include-package-data = true -zip-safe = false [tool.setuptools.dynamic] version = {attr = "account.__version__"} - -[tool.setuptools.package-data] -account = ["locale/*/LC_MESSAGES/*"] From 28d92779d0498425f21b3f07d7bffbc2c18edc92 Mon Sep 17 00:00:00 2001 From: blag Date: Thu, 7 Sep 2023 02:09:13 -0600 Subject: [PATCH 216/239] Bump version to 3.3.0 --- account/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/__init__.py b/account/__init__.py index 1da6a555..88c513ea 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "3.2.1" +__version__ = "3.3.0" From a5e7ee1ec2e5518ca0fbeaa9fa60f225fac9c3fe Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 8 Sep 2023 12:06:37 -0700 Subject: [PATCH 217/239] Include migrations and bump version to 3.3.1 --- CHANGELOG.md | 8 ++++++++ MANIFEST.in | 1 + account/__init__.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d263ac8a..52557c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 3.3.1 + +* #373 Re-include migrations in distribution + +## 3.3.0 + +* #370 Drop Django 2.2, fix timezone-aware comparison, packaging tweaks + ## 3.2.1 * #364 - Performance fix to admin classes diff --git a/MANIFEST.in b/MANIFEST.in index 372ea448..58451c66 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include README.md recursive-include account *.html recursive-include account *.txt recursive-include account/locale * +recursive-include account/migrations * recursive-include docs *.rst exclude tox.ini recursive-exclude django_user_accounts.egg-info * diff --git a/account/__init__.py b/account/__init__.py index 88c513ea..ff041687 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "3.3.0" +__version__ = "3.3.1" From d71857f2021c56a5f670ac86de651c5f5752f074 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 8 Sep 2023 15:25:48 -0700 Subject: [PATCH 218/239] Ignore more files in distribution --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 58451c66..ee1f9ed9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ +global-exclude *.py[cod] include CHANGELOG.md include LICENSE include README.md recursive-include account *.html recursive-include account *.txt recursive-include account/locale * -recursive-include account/migrations * +recursive-include account/migrations *.py recursive-include docs *.rst exclude tox.ini recursive-exclude django_user_accounts.egg-info * From 9168748b0d44facb373f14d772cbe83e19d2e01d Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 8 Sep 2023 15:25:13 -0700 Subject: [PATCH 219/239] Move tests back under account --- .github/workflows/ci.yaml | 2 +- MANIFEST.in | 1 - {tests => account/tests}/__init__.py | 0 {tests => account/tests}/settings.py | 4 ++-- .../account/email/email_confirmation_message.txt | 0 .../account/email/email_confirmation_subject.txt | 0 .../templates/account/email/password_change.txt | 0 .../account/email/password_change_subject.txt | 0 .../tests}/templates/account/email/password_reset.txt | 0 .../account/email/password_reset_subject.txt | 0 .../tests}/templates/account/email_confirm.html | 0 .../templates/account/email_confirmation_sent.html | 0 {tests => account/tests}/templates/account/login.html | 0 .../tests}/templates/account/logout.html | 0 .../tests}/templates/account/password_change.html | 0 .../tests}/templates/account/password_reset_sent.html | 0 .../templates/account/password_reset_token.html | 0 .../templates/account/password_reset_token_fail.html | 0 .../tests}/templates/account/settings.html | 0 .../tests}/templates/account/signup.html | 0 .../tests}/templates/account/signup_closed.html | 0 {tests => account/tests}/test_auth.py | 0 {tests => account/tests}/test_commands.py | 0 {tests => account/tests}/test_decorators.py | 0 {tests => account/tests}/test_email_address.py | 0 {tests => account/tests}/test_models.py | 0 {tests => account/tests}/test_password.py | 0 {tests => account/tests}/test_views.py | 0 {tests => account/tests}/urls.py | 0 makemigrations.py | 4 ++-- manage.py | 2 +- pyproject.toml | 11 ++++++++--- runtests.py | 4 ++-- 33 files changed, 16 insertions(+), 12 deletions(-) rename {tests => account/tests}/__init__.py (100%) rename {tests => account/tests}/settings.py (96%) rename {tests => account/tests}/templates/account/email/email_confirmation_message.txt (100%) rename {tests => account/tests}/templates/account/email/email_confirmation_subject.txt (100%) rename {tests => account/tests}/templates/account/email/password_change.txt (100%) rename {tests => account/tests}/templates/account/email/password_change_subject.txt (100%) rename {tests => account/tests}/templates/account/email/password_reset.txt (100%) rename {tests => account/tests}/templates/account/email/password_reset_subject.txt (100%) rename {tests => account/tests}/templates/account/email_confirm.html (100%) rename {tests => account/tests}/templates/account/email_confirmation_sent.html (100%) rename {tests => account/tests}/templates/account/login.html (100%) rename {tests => account/tests}/templates/account/logout.html (100%) rename {tests => account/tests}/templates/account/password_change.html (100%) rename {tests => account/tests}/templates/account/password_reset_sent.html (100%) rename {tests => account/tests}/templates/account/password_reset_token.html (100%) rename {tests => account/tests}/templates/account/password_reset_token_fail.html (100%) rename {tests => account/tests}/templates/account/settings.html (100%) rename {tests => account/tests}/templates/account/signup.html (100%) rename {tests => account/tests}/templates/account/signup_closed.html (100%) rename {tests => account/tests}/test_auth.py (100%) rename {tests => account/tests}/test_commands.py (100%) rename {tests => account/tests}/test_decorators.py (100%) rename {tests => account/tests}/test_email_address.py (100%) rename {tests => account/tests}/test_models.py (100%) rename {tests => account/tests}/test_password.py (100%) rename {tests => account/tests}/test_views.py (100%) rename {tests => account/tests}/urls.py (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d4900ace..837174bb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: - name: Lint with ruff run: | - ruff --format=github --target-version=py311 account tests + ruff --format=github --target-version=py311 account test: name: Testing diff --git a/MANIFEST.in b/MANIFEST.in index ee1f9ed9..79e1f602 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,6 +9,5 @@ recursive-include account/migrations *.py recursive-include docs *.rst exclude tox.ini recursive-exclude django_user_accounts.egg-info * -recursive-exclude tests * exclude docs/conf.py exclude docs/Makefile diff --git a/tests/__init__.py b/account/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to account/tests/__init__.py diff --git a/tests/settings.py b/account/tests/settings.py similarity index 96% rename from tests/settings.py rename to account/tests/settings.py index 386ad249..6ec3bbf8 100644 --- a/tests/settings.py +++ b/account/tests/settings.py @@ -7,7 +7,7 @@ "django.contrib.sites", "django.contrib.messages", "account", - "tests", + "account.tests", ] DATABASES = { "default": { @@ -16,7 +16,7 @@ } } SITE_ID = 1 -ROOT_URLCONF = "tests.urls" +ROOT_URLCONF = "account.tests.urls" SECRET_KEY = "notasecret" TEMPLATES = [ { diff --git a/tests/templates/account/email/email_confirmation_message.txt b/account/tests/templates/account/email/email_confirmation_message.txt similarity index 100% rename from tests/templates/account/email/email_confirmation_message.txt rename to account/tests/templates/account/email/email_confirmation_message.txt diff --git a/tests/templates/account/email/email_confirmation_subject.txt b/account/tests/templates/account/email/email_confirmation_subject.txt similarity index 100% rename from tests/templates/account/email/email_confirmation_subject.txt rename to account/tests/templates/account/email/email_confirmation_subject.txt diff --git a/tests/templates/account/email/password_change.txt b/account/tests/templates/account/email/password_change.txt similarity index 100% rename from tests/templates/account/email/password_change.txt rename to account/tests/templates/account/email/password_change.txt diff --git a/tests/templates/account/email/password_change_subject.txt b/account/tests/templates/account/email/password_change_subject.txt similarity index 100% rename from tests/templates/account/email/password_change_subject.txt rename to account/tests/templates/account/email/password_change_subject.txt diff --git a/tests/templates/account/email/password_reset.txt b/account/tests/templates/account/email/password_reset.txt similarity index 100% rename from tests/templates/account/email/password_reset.txt rename to account/tests/templates/account/email/password_reset.txt diff --git a/tests/templates/account/email/password_reset_subject.txt b/account/tests/templates/account/email/password_reset_subject.txt similarity index 100% rename from tests/templates/account/email/password_reset_subject.txt rename to account/tests/templates/account/email/password_reset_subject.txt diff --git a/tests/templates/account/email_confirm.html b/account/tests/templates/account/email_confirm.html similarity index 100% rename from tests/templates/account/email_confirm.html rename to account/tests/templates/account/email_confirm.html diff --git a/tests/templates/account/email_confirmation_sent.html b/account/tests/templates/account/email_confirmation_sent.html similarity index 100% rename from tests/templates/account/email_confirmation_sent.html rename to account/tests/templates/account/email_confirmation_sent.html diff --git a/tests/templates/account/login.html b/account/tests/templates/account/login.html similarity index 100% rename from tests/templates/account/login.html rename to account/tests/templates/account/login.html diff --git a/tests/templates/account/logout.html b/account/tests/templates/account/logout.html similarity index 100% rename from tests/templates/account/logout.html rename to account/tests/templates/account/logout.html diff --git a/tests/templates/account/password_change.html b/account/tests/templates/account/password_change.html similarity index 100% rename from tests/templates/account/password_change.html rename to account/tests/templates/account/password_change.html diff --git a/tests/templates/account/password_reset_sent.html b/account/tests/templates/account/password_reset_sent.html similarity index 100% rename from tests/templates/account/password_reset_sent.html rename to account/tests/templates/account/password_reset_sent.html diff --git a/tests/templates/account/password_reset_token.html b/account/tests/templates/account/password_reset_token.html similarity index 100% rename from tests/templates/account/password_reset_token.html rename to account/tests/templates/account/password_reset_token.html diff --git a/tests/templates/account/password_reset_token_fail.html b/account/tests/templates/account/password_reset_token_fail.html similarity index 100% rename from tests/templates/account/password_reset_token_fail.html rename to account/tests/templates/account/password_reset_token_fail.html diff --git a/tests/templates/account/settings.html b/account/tests/templates/account/settings.html similarity index 100% rename from tests/templates/account/settings.html rename to account/tests/templates/account/settings.html diff --git a/tests/templates/account/signup.html b/account/tests/templates/account/signup.html similarity index 100% rename from tests/templates/account/signup.html rename to account/tests/templates/account/signup.html diff --git a/tests/templates/account/signup_closed.html b/account/tests/templates/account/signup_closed.html similarity index 100% rename from tests/templates/account/signup_closed.html rename to account/tests/templates/account/signup_closed.html diff --git a/tests/test_auth.py b/account/tests/test_auth.py similarity index 100% rename from tests/test_auth.py rename to account/tests/test_auth.py diff --git a/tests/test_commands.py b/account/tests/test_commands.py similarity index 100% rename from tests/test_commands.py rename to account/tests/test_commands.py diff --git a/tests/test_decorators.py b/account/tests/test_decorators.py similarity index 100% rename from tests/test_decorators.py rename to account/tests/test_decorators.py diff --git a/tests/test_email_address.py b/account/tests/test_email_address.py similarity index 100% rename from tests/test_email_address.py rename to account/tests/test_email_address.py diff --git a/tests/test_models.py b/account/tests/test_models.py similarity index 100% rename from tests/test_models.py rename to account/tests/test_models.py diff --git a/tests/test_password.py b/account/tests/test_password.py similarity index 100% rename from tests/test_password.py rename to account/tests/test_password.py diff --git a/tests/test_views.py b/account/tests/test_views.py similarity index 100% rename from tests/test_views.py rename to account/tests/test_views.py diff --git a/tests/urls.py b/account/tests/urls.py similarity index 100% rename from tests/urls.py rename to account/tests/urls.py diff --git a/makemigrations.py b/makemigrations.py index 894b4d75..eb2aa60d 100644 --- a/makemigrations.py +++ b/makemigrations.py @@ -11,7 +11,7 @@ "django.contrib.contenttypes", "django.contrib.sites", "account", - "tests" + "account.tests" ], MIDDLEWARE_CLASSES=[], DATABASES={ @@ -21,7 +21,7 @@ } }, SITE_ID=1, - ROOT_URLCONF="tests.urls", + ROOT_URLCONF="account.tests.urls", SECRET_KEY="notasecret", ) diff --git a/manage.py b/manage.py index dc935d61..646bf7ab 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") from django.core.management import execute_from_command_line diff --git a/pyproject.toml b/pyproject.toml index cdb037a4..ef886100 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,8 @@ skip_glob = "account/migrations/*,docs" include_trailing_comma = "True" [tool.pytest.ini_options] -testpaths = ["tests"] -DJANGO_SETTINGS_MODULE = "tests.settings" +testpaths = ["account/tests"] +DJANGO_SETTINGS_MODULE = "account.tests.settings" [tool.ruff] line-length = 120 @@ -57,7 +57,12 @@ line-length = 120 "account/migrations/**.py" = ["E501"] [tool.setuptools] -packages = ["account"] +package-dir = {"" = "."} +include-package-data = true +zip-safe = false [tool.setuptools.dynamic] version = {attr = "account.__version__"} + +[tool.setuptools.package-data] +account = ["locale/*/LC_MESSAGES/*"] diff --git a/runtests.py b/runtests.py index 1eb34476..5a60937d 100644 --- a/runtests.py +++ b/runtests.py @@ -6,7 +6,7 @@ def runtests(*test_args): - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "account.tests.settings") django.setup() parent = os.path.dirname(os.path.abspath(__file__)) @@ -15,7 +15,7 @@ def runtests(*test_args): from django.test.runner import DiscoverRunner runner_class = DiscoverRunner if not test_args: - test_args = ["tests"] + test_args = ["account/tests"] failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) sys.exit(failures) From 714c307e36af406783ad8a80cecf795206cc389d Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 8 Sep 2023 15:33:00 -0700 Subject: [PATCH 220/239] Configure DeepSource to ignore test files --- .deepsource.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 2783c4df..9e84a0db 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -3,7 +3,8 @@ version = 1 exclude_patterns = [ "makemigrations.py", "runtests.py", - "tests/**", + "account/tests/**", + "account/tests/test_*.py", ] [[analyzers]] From b79d26e0c45e309166c8a19c8e6f18eeb32616c0 Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 8 Sep 2023 16:01:05 -0700 Subject: [PATCH 221/239] Use runtests.py in GHA --- .github/workflows/ci.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 837174bb..0cc1bb37 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,10 +51,6 @@ jobs: shell: bash run: pip install Django==${{ matrix.django }} 'django-appconf>=1.0.4' 'pytz>=2020.4' - - name: Install test utilities - shell: bash - run: pip install pytest pytest-django - - name: Running Python Tests shell: bash - run: pytest + run: python3 runtests.py From 0d6e77e442747ce5119327e86ad7a7740b44f26a Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 13:59:09 -0700 Subject: [PATCH 222/239] Include migration for SignupCode.max_uses --- .../0006_alter_signupcode_max_uses.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 account/migrations/0006_alter_signupcode_max_uses.py diff --git a/account/migrations/0006_alter_signupcode_max_uses.py b/account/migrations/0006_alter_signupcode_max_uses.py new file mode 100644 index 00000000..67d84636 --- /dev/null +++ b/account/migrations/0006_alter_signupcode_max_uses.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-09-12 20:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_update_default_language'), + ] + + operations = [ + migrations.AlterField( + model_name='signupcode', + name='max_uses', + field=models.PositiveIntegerField(default=1, verbose_name='max uses'), + ), + ] From 5a6e5653867b825b44299c5662c656de2f6729dd Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 15:36:47 -0700 Subject: [PATCH 223/239] Fix PYL-E0101 --- account/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/fields.py b/account/fields.py index c9b5ef3f..fe80486d 100644 --- a/account/fields.py +++ b/account/fields.py @@ -13,4 +13,4 @@ def __init__(self, *args, **kwargs): "blank": True, } defaults.update(kwargs) - return super(TimeZoneField, self).__init__(*args, **defaults) + super(TimeZoneField, self).__init__(*args, **defaults) From e11dcc88e3b60e57f96e49d90e2467ffabba9ebd Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 15:37:19 -0700 Subject: [PATCH 224/239] Fix PYL-W0612 --- account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/models.py b/account/models.py index 02fe4c8b..8f8eeb70 100644 --- a/account/models.py +++ b/account/models.py @@ -388,7 +388,7 @@ def expunge(cls, hours_ago=None): @classmethod def mark(cls, user): - account_deletion, created = cls.objects.get_or_create(user=user) + account_deletion, created = cls.objects.get_or_create(user=user) # skipcq: PYL-W0612 account_deletion.email = user.email account_deletion.save() hookset.account_delete_mark(account_deletion) From 3cdf789c263065f13c5b501a9c9a96e4d1dc91eb Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 15:59:20 -0700 Subject: [PATCH 225/239] Fix PYL-W0613 --- account/models.py | 2 +- account/templatetags/account_tags.py | 2 +- account/views.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/account/models.py b/account/models.py index 8f8eeb70..6a0b7b1e 100644 --- a/account/models.py +++ b/account/models.py @@ -90,7 +90,7 @@ def localtime(self, value): @receiver(post_save, sender=settings.AUTH_USER_MODEL) -def user_post_save(sender, **kwargs): +def user_post_save(*args, **kwargs): """ After User.save is called we check to see if it was a created user. If so, we check if the User object wants account creation. If all passes we diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index a98ad35a..027ee76a 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -25,7 +25,7 @@ def render(self, context): @register.tag(name="user_display") -def do_user_display(parser, token): +def do_user_display(parser, token): # skipcq: PYL-W0613 """ Example usage:: diff --git a/account/views.py b/account/views.py index d448664a..01b3a989 100644 --- a/account/views.py +++ b/account/views.py @@ -274,7 +274,7 @@ def create_user(self, form, commit=True, model=None, **kwargs): user.save() return user - def create_account(self, form): + def create_account(self, form): # skipcq: PYL-W0613 return Account.create(request=self.request, user=self.created_user, create_email=False) def generate_username(self, form): @@ -283,7 +283,7 @@ def generate_username(self, form): "Override SignupView.generate_username in a subclass." ) - def create_email_address(self, form, **kwargs): + def create_email_address(self, form, **kwargs): # skipcq: PYL-W0613 kwargs.setdefault("primary", True) kwargs.setdefault("verified", False) if self.signup_code: From b55ba5d2fffb1b70a9d81505eb70431ad9e81a75 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:16:54 -0700 Subject: [PATCH 226/239] Fix PYL-R0201 --- account/conf.py | 3 ++- account/hooks.py | 27 ++++++++++++++++++--------- account/middleware.py | 9 ++++++--- account/templatetags/account_tags.py | 3 ++- account/views.py | 9 ++++++--- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/account/conf.py b/account/conf.py index ff711b1f..e0b28182 100644 --- a/account/conf.py +++ b/account/conf.py @@ -55,5 +55,6 @@ class AccountAppConf(AppConf): TIMEZONES = TIMEZONES LANGUAGES = LANGUAGES - def configure_hookset(self, value): + @staticmethod + def configure_hookset(value): return load_path_attr(value)() diff --git a/account/hooks.py b/account/hooks.py index 3dc70943..9817eedd 100644 --- a/account/hooks.py +++ b/account/hooks.py @@ -11,30 +11,35 @@ class AccountDefaultHookSet(object): - def send_invitation_email(self, to, ctx): + @staticmethod + def send_invitation_email(to, ctx): subject = render_to_string("account/email/invite_user_subject.txt", ctx) message = render_to_string("account/email/invite_user.txt", ctx) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) - def send_confirmation_email(self, to, ctx): + @staticmethod + def send_confirmation_email(to, ctx): subject = render_to_string("account/email/email_confirmation_subject.txt", ctx) subject = "".join(subject.splitlines()) # remove superfluous line breaks message = render_to_string("account/email/email_confirmation_message.txt", ctx) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) - def send_password_change_email(self, to, ctx): + @staticmethod + def send_password_change_email(to, ctx): subject = render_to_string("account/email/password_change_subject.txt", ctx) subject = "".join(subject.splitlines()) message = render_to_string("account/email/password_change.txt", ctx) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) - def send_password_reset_email(self, to, ctx): + @staticmethod + def send_password_reset_email(to, ctx): subject = render_to_string("account/email/password_reset_subject.txt", ctx) subject = "".join(subject.splitlines()) message = render_to_string("account/email/password_reset.txt", ctx) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, to) - def generate_random_token(self, extra=None, hash_func=hashlib.sha256): + @staticmethod + def generate_random_token(extra=None, hash_func=hashlib.sha256): if extra is None: extra = [] bits = extra + [str(random.SystemRandom().getrandbits(512))] @@ -49,22 +54,26 @@ def generate_signup_code_token(self, email=None): def generate_email_confirmation_token(self, email): return self.generate_random_token([email]) - def get_user_credentials(self, form, identifier_field): + @staticmethod + def get_user_credentials(form, identifier_field): return { "username": form.cleaned_data[identifier_field], "password": form.cleaned_data["password"], } - def clean_password(self, password_new, password_new_confirm): + @staticmethod + def clean_password(password_new, password_new_confirm): if password_new != password_new_confirm: raise forms.ValidationError(_("You must type the same password each time.")) return password_new - def account_delete_mark(self, deletion): + @staticmethod + def account_delete_mark(deletion): deletion.user.is_active = False deletion.user.save() - def account_delete_expunge(self, deletion): + @staticmethod + def account_delete_expunge(deletion): deletion.user.delete() diff --git a/account/middleware.py b/account/middleware.py index 1e70d2b0..bedcee16 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -24,7 +24,8 @@ class LocaleMiddleware(BaseMiddleware): (if the language is available, of course). """ - def get_language_for_user(self, request): + @staticmethod + def get_language_for_user(request): if request.user.is_authenticated: try: account = Account.objects.get(user=request.user) @@ -37,7 +38,8 @@ def process_request(self, request): translation.activate(self.get_language_for_user(request)) request.LANGUAGE_CODE = translation.get_language() - def process_response(self, request, response): + @staticmethod + def process_response(request, response): patch_vary_headers(response, ("Accept-Language",)) response["Content-Language"] = translation.get_language() translation.deactivate() @@ -50,7 +52,8 @@ class TimezoneMiddleware(BaseMiddleware): templates to the user's timezone. """ - def process_request(self, request): + @staticmethod + def process_request(request): try: account = getattr(request.user, "account", None) except Account.DoesNotExist: diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 027ee76a..00acea18 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -51,7 +51,8 @@ def do_user_display(parser, token): # skipcq: PYL-W0613 class URLNextNode(URLNode): - def add_next(self, url, context): + @staticmethod + def add_next(url, context): """ With both `redirect_field_name` and `redirect_field_value` available in the context, add on a querystring to handle "next" redirecting. diff --git a/account/views.py b/account/views.py index 01b3a989..b37c5b96 100644 --- a/account/views.py +++ b/account/views.py @@ -409,7 +409,8 @@ def form_valid(self, form): self.after_login(form) return redirect(self.get_success_url()) - def after_login(self, form): + @staticmethod + def after_login(form): signals.user_logged_in.send(sender=LoginView, user=form.user, form=form) def get_success_url(self, fallback_url=None, **kwargs): @@ -533,7 +534,8 @@ def get_object(self, queryset=None): except EmailConfirmation.DoesNotExist: raise Http404() - def get_queryset(self): + @staticmethod + def get_queryset(): qs = EmailConfirmation.objects.all() qs = qs.select_related("email_address__user") return qs @@ -550,7 +552,8 @@ def get_redirect_url(self): return settings.ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL return settings.ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL - def after_confirmation(self, confirmation): + @staticmethod + def after_confirmation(confirmation): user = confirmation.email_address.user user.is_active = True user.save() From 630615038577e24d0921384adf330f367bfe8536 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:17:46 -0700 Subject: [PATCH 227/239] Fix PYL-R0205 --- account/hooks.py | 4 ++-- account/mixins.py | 2 +- account/views.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/account/hooks.py b/account/hooks.py index 9817eedd..170942aa 100644 --- a/account/hooks.py +++ b/account/hooks.py @@ -9,7 +9,7 @@ from account.conf import settings -class AccountDefaultHookSet(object): +class AccountDefaultHookSet: @staticmethod def send_invitation_email(to, ctx): @@ -77,7 +77,7 @@ def account_delete_expunge(deletion): deletion.user.delete() -class HookProxy(object): +class HookProxy: def __getattr__(self, attr): return getattr(settings.ACCOUNT_HOOKSET, attr) diff --git a/account/mixins.py b/account/mixins.py index 0600b10a..3250880e 100644 --- a/account/mixins.py +++ b/account/mixins.py @@ -4,7 +4,7 @@ from account.utils import handle_redirect_to_login -class LoginRequiredMixin(object): +class LoginRequiredMixin: redirect_field_name = REDIRECT_FIELD_NAME login_url = None diff --git a/account/views.py b/account/views.py index b37c5b96..6ada81aa 100644 --- a/account/views.py +++ b/account/views.py @@ -39,7 +39,7 @@ from account.utils import default_redirect, get_form_data, is_ajax -class PasswordMixin(object): +class PasswordMixin: """ Mixin handling common elements of password change. From c3540778e33e9f634de49d62e5f29c69208e9d83 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:19:51 -0700 Subject: [PATCH 228/239] Fix PTC-W0016 --- account/templatetags/account_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 00acea18..a4674be6 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -57,7 +57,7 @@ def add_next(url, context): With both `redirect_field_name` and `redirect_field_value` available in the context, add on a querystring to handle "next" redirecting. """ - if all([key in context for key in ["redirect_field_name", "redirect_field_value"]]): + if all(key in context for key in ["redirect_field_name", "redirect_field_value"]): if context["redirect_field_value"]: url += "?" + urlencode({ context["redirect_field_name"]: context["redirect_field_value"], From 7788456bbb3364e4eee95c9742e3d76d5a5fb135 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:22:48 -0700 Subject: [PATCH 229/239] Fix PYL-R1705 --- account/models.py | 3 +-- account/templatetags/account_tags.py | 3 +-- account/utils.py | 28 +++++++++++++--------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/account/models.py b/account/models.py index 6a0b7b1e..8c263ccd 100644 --- a/account/models.py +++ b/account/models.py @@ -149,8 +149,7 @@ class Meta: def __str__(self): if self.email: return "{0} [{1}]".format(self.email, self.code) - else: - return self.code + return self.code @classmethod def exists(cls, code=None, email=None): diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index a4674be6..697c1024 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -73,8 +73,7 @@ def render(self, context): if self.asvar: context[self.asvar] = url return "" - else: - return url + return url @register.tag diff --git a/account/utils.py b/account/utils.py index c9e67d78..01855ac8 100644 --- a/account/utils.py +++ b/account/utils.py @@ -39,19 +39,18 @@ def default_redirect(request, fallback_url, **kwargs): ) if next_url and is_safe(next_url): return next_url - else: - try: - fallback_url = reverse(fallback_url) - except NoReverseMatch: - if callable(fallback_url): - raise - if "/" not in fallback_url and "." not in fallback_url: - raise - # assert the fallback URL is safe to return to caller. if it is - # determined unsafe then raise an exception as the fallback value comes - # from the a source the developer choose. - is_safe(fallback_url, raise_on_fail=True) - return fallback_url + try: + fallback_url = reverse(fallback_url) + except NoReverseMatch: + if callable(fallback_url): + raise + if "/" not in fallback_url and "." not in fallback_url: + raise + # assert the fallback URL is safe to return to caller. if it is + # determined unsafe then raise an exception as the fallback value comes + # from the a source the developer choose. + is_safe(fallback_url, raise_on_fail=True) + return fallback_url def user_display(user): @@ -144,5 +143,4 @@ def check_password_expired(user): if expiration < now: return True - else: - return False + return False From 9fe08f419d99b03b4ac3177e852e10c29e4b12b7 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:23:29 -0700 Subject: [PATCH 230/239] Fix PYL-R1703 --- account/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/account/utils.py b/account/utils.py index 01855ac8..91ae8094 100644 --- a/account/utils.py +++ b/account/utils.py @@ -141,6 +141,4 @@ def check_password_expired(user): now = timezone.now() expiration = latest.timestamp + datetime.timedelta(seconds=expiry) - if expiration < now: - return True - return False + return bool(expiration < now) From 4bf6459a5a5a4d2a3a423566f25bac5328abe2ba Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:24:10 -0700 Subject: [PATCH 231/239] Fix PTC-W0048 --- account/forms.py | 9 ++++++--- account/middleware.py | 8 ++++---- account/templatetags/account_tags.py | 11 ++++++----- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/account/forms.py b/account/forms.py index e7786ceb..14f2f2a1 100644 --- a/account/forms.py +++ b/account/forms.py @@ -81,9 +81,12 @@ def clean_email(self): raise forms.ValidationError(_("A user is registered with this email address.")) def clean(self): - if "password" in self.cleaned_data and "password_confirm" in self.cleaned_data: - if self.cleaned_data["password"] != self.cleaned_data["password_confirm"]: - raise forms.ValidationError(_("You must type the same password each time.")) + if ( + "password" in self.cleaned_data and + "password_confirm" in self.cleaned_data and + self.cleaned_data["password"] != self.cleaned_data["password_confirm"] + ): + raise forms.ValidationError(_("You must type the same password each time.")) return self.cleaned_data diff --git a/account/middleware.py b/account/middleware.py index bedcee16..6d998586 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -72,10 +72,10 @@ def process_request(self, request): # Authenticated users must be allowed to access # "change password" page and "log out" page. # even if password is expired. - if next_url not in [settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, - settings.ACCOUNT_LOGOUT_URL, - ]: - if check_password_expired(request.user): + if next_url not in [ + settings.ACCOUNT_PASSWORD_CHANGE_REDIRECT_URL, + settings.ACCOUNT_LOGOUT_URL, + ] and check_password_expired(request.user): signals.password_expired.send(sender=self, user=request.user) messages.add_message( request, diff --git a/account/templatetags/account_tags.py b/account/templatetags/account_tags.py index 697c1024..99d307dc 100644 --- a/account/templatetags/account_tags.py +++ b/account/templatetags/account_tags.py @@ -57,11 +57,12 @@ def add_next(url, context): With both `redirect_field_name` and `redirect_field_value` available in the context, add on a querystring to handle "next" redirecting. """ - if all(key in context for key in ["redirect_field_name", "redirect_field_value"]): - if context["redirect_field_value"]: - url += "?" + urlencode({ - context["redirect_field_name"]: context["redirect_field_value"], - }) + if all( + key in context for key in ["redirect_field_name", "redirect_field_value"] + ) and context["redirect_field_value"]: + url += "?" + urlencode({ + context["redirect_field_name"]: context["redirect_field_value"], + }) return url def render(self, context): From 06be2cdf79ff281dd88d22a13099bf8ef83839e0 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 12 Sep 2023 16:24:40 -0700 Subject: [PATCH 232/239] Fix PTC-W0906 --- .../0007_alter_emailconfirmation_sent.py | 18 ++++++++++++++++++ account/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 account/migrations/0007_alter_emailconfirmation_sent.py diff --git a/account/migrations/0007_alter_emailconfirmation_sent.py b/account/migrations/0007_alter_emailconfirmation_sent.py new file mode 100644 index 00000000..b2e0d627 --- /dev/null +++ b/account/migrations/0007_alter_emailconfirmation_sent.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-09-12 21:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_alter_signupcode_max_uses'), + ] + + operations = [ + migrations.AlterField( + model_name='emailconfirmation', + name='sent', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index 8c263ccd..fea2e976 100644 --- a/account/models.py +++ b/account/models.py @@ -309,7 +309,7 @@ class EmailConfirmation(models.Model): email_address = models.ForeignKey(EmailAddress, on_delete=models.CASCADE) created = models.DateTimeField(default=timezone.now) - sent = models.DateTimeField(null=True) + sent = models.DateTimeField(blank=True, null=True) key = models.CharField(max_length=64, unique=True) objects = EmailConfirmationManager() From ffc35d7fe33750ec84ba382a0daa905352ba3260 Mon Sep 17 00:00:00 2001 From: blag Date: Thu, 28 Sep 2023 10:00:55 -0700 Subject: [PATCH 233/239] Update changelog and bump version to 3.3.2 --- CHANGELOG.md | 7 ++++++- account/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52557c40..b68b889d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,14 @@ BI indicates a backward incompatible change. Take caution when upgrading to a version with these. Your code will need to be updated to continue working. +## 3.3.2 + +* #375 - Include migration for `SignupCode.max_uses` (closes #374) +* #376 - Static analysis fixups + ## 3.3.1 -* #373 Re-include migrations in distribution +* #373 - Re-include migrations in distribution ## 3.3.0 diff --git a/account/__init__.py b/account/__init__.py index ff041687..3e2d550b 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -__version__ = "3.3.1" +__version__ = "3.3.2" From 7526a552b9e3000285d76b2575639b749caa9208 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 11 Oct 2023 22:25:57 +0200 Subject: [PATCH 234/239] fixed table rendering issue in Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f400192..1776e548 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Pinax is an open-source platform built on the Django Web Framework. It is an eco Django / Python | 3.8 | 3.9 | 3.10 | 3.11 --------------- | --- | --- | ---- | ---- - 3.2 | * | * | * | - 4.2 | * | * | * | * +3.2 | * | * | * | +4.2 | * | * | * | * ## Requirements From 9ab626007e9605e76cba1f9420c0687673ff264b Mon Sep 17 00:00:00 2001 From: blag Date: Fri, 15 Dec 2023 17:01:48 -0800 Subject: [PATCH 235/239] Fix rST syntax complaints --- docs/settings.rst | 2 +- docs/signals.rst | 2 +- docs/templates.rst | 71 ++++++++++++++++++++++++++++++---------------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 7ad7086b..411dac83 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -220,5 +220,5 @@ the user. Default: ``False`` This setting will make new registrations inactive, until staff will set ``is_active`` - flag in admin panel. Additional integration (like sending notifications to staff) +flag in admin panel. Additional integration (like sending notifications to staff) is possible with ``account.signals.user_signed_up`` signal. diff --git a/docs/signals.rst b/docs/signals.rst index 9b7b8f09..356d6b7b 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -52,7 +52,7 @@ email_confirmed --------------- Triggered when a user confirmed an email. Providing argument -``email_address``(EmailAddress instance). +``email_address`` (EmailAddress instance). email_confirmation_sent diff --git a/docs/templates.rst b/docs/templates.rst index 550a033a..f416db31 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -22,7 +22,8 @@ templates yourself. Login/Registration/Signup Templates ----------------------------------- -**account/login.html** +``account/login.html`` +~~~~~~~~~~~~~~~~~~~~~~ The template with the form to authenticate the user. The template has the following context: @@ -32,16 +33,18 @@ following context: ``redirect_field_name`` The name of the hidden field that will hold the url where to redirect the -user after login. + user after login. ``redirect_field_value`` The actual url where the user will be redirected after login. -**account/logout.html** +``account/logout.html`` +~~~~~~~~~~~~~~~~~~~~~~~ The default template shown after the user has been logged out. -**account/signup.html** +``account/signup.html`` +~~~~~~~~~~~~~~~~~~~~~~~ The template with the form to registrate a new user. The template has the following context: @@ -51,12 +54,13 @@ following context: ``redirect_field_name`` The name of the hidden field that will hold the url where to redirect the -user after signing up. + user after signing up. ``redirect_field_value`` The actual url where the user will be redirected after signing up. -**account/signup_closed.html** +``account/signup_closed.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template to inform the user that creating new users is not allowed (mainly because ``settings.ACCOUNT_OPEN_SIGNUP`` is ``False``). @@ -64,7 +68,8 @@ because ``settings.ACCOUNT_OPEN_SIGNUP`` is ``False``). Email Confirmation Templates ---------------------------- -**account/email_confirm.html** +``account/email_confirm.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template to confirm an email address. The template has the following context: @@ -74,7 +79,8 @@ A template to confirm an email address. The template has the following context: ``confirmation`` The EmailConfirmation instance to be confirmed. -**account/email_confirmation_sent.html** +``account/email_confirmation_sent.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The template shown after a new user has been created. It should tell the user that an activation link has been sent to his email address. The template has @@ -85,9 +91,10 @@ the following context: ``success_url`` A url where the user can be redirected from this page. For example to -show a link to go back. + show a link to go back. -**account/email_confirmed.html** +``account/email_confirmed.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template shown after an email address has been confirmed. The template context is the same as in email_confirm.html. @@ -98,7 +105,8 @@ context is the same as in email_confirm.html. Password Management Templates ----------------------------- -**account/password_change.html** +``account/password_change.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The template that shows the form to change the user's password, when the user is authenticated. The template has the following context: @@ -106,7 +114,8 @@ is authenticated. The template has the following context: ``form`` The form to change the password. -**account/password_reset.html** +``account/password_reset.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template with a form to type an email address to reset a user's password. The template has the following context: @@ -114,7 +123,8 @@ The template has the following context: ``form`` The form to reset the password. -**account/password_reset_sent.html** +``account/password_reset_sent.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template to inform the user that his password has been reset and that he should receive an email with a link to create a new password. The template has @@ -122,12 +132,13 @@ the following context: ``form`` An instance of ``PasswordResetForm``. Usually the fields of this form -must be hidden. + must be hidden. ``resend`` If ``True`` it means that the reset link has been resent to the user. -**account/password_reset_token.html** +``account/password_reset_token.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The template that shows the form to change the user's password. The user should have come here following the link received to reset his password. The template @@ -136,7 +147,8 @@ has the following context: ``form`` The form to set the new password. -**account/password_reset_token_fail.html** +``account/password_reset_token_fail.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A template to inform the user that he is not allowed to change the password, because the authentication token is wrong. The template has the following @@ -148,7 +160,8 @@ context: Account Settings ---------------- -**account/settings.html** +``account/settings.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~ A template with a form where the user may change his email address, time zone and preferred language. The template has the following context: @@ -159,7 +172,8 @@ and preferred language. The template has the following context: Emails (actual emails themselves) --------------------------------- -**account/email/email_confirmation_subject.txt** +``account/email/email_confirmation_subject.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The subject line of the email that will be sent to the new user to validate the email address. It will be rendered as a single line. The template has the @@ -180,12 +194,14 @@ following context: ``key`` The confirmation key. -**account/email/email_confirmation_message.txt** +``account/email/email_confirmation_message.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The body of the activation email. It has the same context as the subject template (see above). -**account/email/invite_user.txt** +``account/email/invite_user.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The body of the invitation sent to somebody to join the site. The template has the following context: @@ -199,12 +215,14 @@ the following context: ``signup_url`` The link used to use the invitation and create a new account. -**account/email/invite_user_subject.txt** +``account/email/invite_user_subject.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The subject line of the invitation sent to somebody to join the site. The template has the same context as in invite_user.txt. -**account/email/password_change.txt** +``account/email/password_change.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The body of the email used to inform the user that his password has been changed. The template has the following context: @@ -218,12 +236,14 @@ changed. The template has the following context: ``current_site`` The instance of django.contrib.sites.models.Site that identifies the site. -**account/email/password_change_subject.txt** +``account/email/password_change_subject.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The subject line of the email used to inform the user that his password has been changed. The context is the same as in password_change.txt. -**account/email/password_reset.txt** +``account/email/password_reset.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The body of the email with a link to reset a user's password. The template has the following context: @@ -238,7 +258,8 @@ the following context: ``password_reset_url`` The link that the user needs to follow to set a new password. -**account/email/password_reset_subject.txt** +``account/email/password_reset_subject.txt`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The subject line of the email with a link to reset a user's password. The context is the same as in password_reset.txt. From 380c95109a3bd490cea74bd0fff73b9c46221c74 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 9 Jan 2024 02:14:37 -0800 Subject: [PATCH 236/239] Document admin approval templates --- docs/templates.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/templates.rst b/docs/templates.rst index f416db31..20cd05b2 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -65,6 +65,33 @@ following context: A template to inform the user that creating new users is not allowed (mainly because ``settings.ACCOUNT_OPEN_SIGNUP`` is ``False``). + +Registration Approval Templates +------------------------------- + +These templates are only used when ``settings.ACCOUNT_APPROVAL_REQUIRED`` is +``True``. + +``account/admin_approval_sent.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The template shown after a new user has been created (with ``is_active`` set to +``False``). It should explain that an administrator will need to approve their +registration before they can use it. The template has the following context: + +``email`` + The email address for the newly created user. + +``success_url`` + The URL where the user will be directed to. + +``account/ajax/admin_approval_sent.html`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The same template as ``account/admin_approval_sent.html`` but for AJAX +responses; is rendered with the same context. + + Email Confirmation Templates ---------------------------- From 883f8f0cad55a50cb36896a130709fa1d43c8a65 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 9 Jan 2024 02:14:53 -0800 Subject: [PATCH 237/239] Fixup formatting --- docs/templates.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/templates.rst b/docs/templates.rst index 20cd05b2..aa8cc2db 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -11,6 +11,7 @@ to build from. Note, this document assumes you have read the installation docs. .. _pinax-theme-bootstrap: https://github.com/pinax/pinax-theme-bootstrap .. _starting point: https://github.com/pinax/pinax-theme-bootstrap/tree/master/pinax_theme_bootstrap/templates/account + Template Files =============== @@ -107,7 +108,7 @@ A template to confirm an email address. The template has the following context: The EmailConfirmation instance to be confirmed. ``account/email_confirmation_sent.html`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The template shown after a new user has been created. It should tell the user that an activation link has been sent to his email address. The template has @@ -184,6 +185,7 @@ context: ``url`` The url to request a new reset token. + Account Settings ---------------- @@ -196,6 +198,7 @@ and preferred language. The template has the following context: ``form`` The form to change the settings. + Emails (actual emails themselves) --------------------------------- @@ -275,7 +278,6 @@ been changed. The context is the same as in password_change.txt. The body of the email with a link to reset a user's password. The template has the following context: - ``user`` The user whom the password belongs to. @@ -291,6 +293,7 @@ the following context: The subject line of the email with a link to reset a user's password. The context is the same as in password_reset.txt. + Template Tags ============= From 6cab30087df4e62bf38dcb4242be128e0a79fef7 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 9 Jan 2024 02:15:24 -0800 Subject: [PATCH 238/239] Update docs/conf.py --- docs/conf.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 46109091..431a23c8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +import datetime import os import sys @@ -8,17 +9,17 @@ master_doc = "index" project = "django-user-accounts" copyright_holder = "James Tauber and contributors" -copyright = "2014, {0}".format(copyright_holder) +copyright = f"{datetime.datetime.now().year}, {copyright_holder}" exclude_patterns = ["_build"] pygments_style = "sphinx" html_theme = "default" -htmlhelp_basename = "{0}doc".format(project) +htmlhelp_basename = f"{project}doc" latex_documents = [ - ("index", "{0}.tex".format(project), "{0} Documentation".format(project), + ("index", f"{project}.tex", f"{project} Documentation", "Pinax", "manual"), ] man_pages = [ - ("index", project, "{0} Documentation".format(project), + ("index", project, f"{project} Documentation", ["Pinax"], 1) ] From 91e0c1c1b2f03c0afe609ed59d55b077fccfd745 Mon Sep 17 00:00:00 2001 From: blag Date: Tue, 9 Jan 2024 02:27:10 -0800 Subject: [PATCH 239/239] Use account.utils.is_ajax --- account/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views.py b/account/views.py index d7c76477..5fbf6473 100644 --- a/account/views.py +++ b/account/views.py @@ -361,7 +361,7 @@ def closed(self): return self.response_class(**response_kwargs) def account_approval_required_response(self): - if self.request.is_ajax(): + if is_ajax(self.request): template_name = self.template_name_admin_approval_sent_ajax else: template_name = self.template_name_admin_approval_sent