From 0f60c0e42a5a6d282b7f66efddcca99675fd26d7 Mon Sep 17 00:00:00 2001 From: ellmetha Date: Sun, 26 Oct 2014 15:07:45 +0100 Subject: [PATCH 01/34] Text/html alternative added for recovery email --- docs/views.rst | 4 + .../templates/password_reset/base_email.html | 184 ++++++++++++++++++ .../password_reset/recovery_email.html | 10 + password_reset/tests/tests.py | 2 + password_reset/views.py | 17 +- 5 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 password_reset/templates/password_reset/base_email.html create mode 100644 password_reset/templates/password_reset/recovery_email.html diff --git a/docs/views.rst b/docs/views.rst index 8490d30d..0f03a759 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -43,6 +43,10 @@ Attributes * ``email_template_name``: the template to use for sending the reset link by email. Default: ``password_reset/recovery_email.txt``. +* ``email_html_template_name``: the HTML template to use for sending the reset link by + email. The content of this template will be sent as the text/html content type alternative. + Default: ``password_reset/recovery_email.html``. + * ``email_subject_template_name``: the template to use for generating the email subject. Defaults to ``password_reset/recovery_email_subject.txt``. diff --git a/password_reset/templates/password_reset/base_email.html b/password_reset/templates/password_reset/base_email.html new file mode 100644 index 00000000..da53587a --- /dev/null +++ b/password_reset/templates/password_reset/base_email.html @@ -0,0 +1,184 @@ +{% load i18n %} + + + + + + {% block title %}{% endblock title %} + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ {% block content %} + {% endblock content %} +
+
+ + + diff --git a/password_reset/templates/password_reset/recovery_email.html b/password_reset/templates/password_reset/recovery_email.html new file mode 100644 index 00000000..1313e2c5 --- /dev/null +++ b/password_reset/templates/password_reset/recovery_email.html @@ -0,0 +1,10 @@ +{% extends "password_reset/base_email.html" %} +{% load i18n %} +{% load url from future %} + +{% block content %} +

{% blocktrans %}Dear {{ username }},{% endblocktrans %}

+

{% blocktrans with domain=site.domain %}You -- or someone pretending to be you -- has requested a password reset on {{ domain }}.{% endblocktrans %}

+

{% trans "You can set your new password by following this link:" %} http{% if secure %}s{% endif %}://{{ site.domain }}{% url "password_reset_reset" token %}

+

{% trans "If you don't want to reset your password, simply ignore this email and it will stay unchanged." %}

+{% endblock content %} \ No newline at end of file diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index 473152d4..2e52814e 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -275,6 +275,8 @@ def test_email_recover(self): url, {'username_or_email': 'bar@example.com'}, follow=True, ) self.assertEqual(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox[0].alternatives), 1) + self.assertEqual(mail.outbox[0].alternatives[0][1], 'text/html') self.assertEqual(len(response.redirect_chain), 1) self.assertContains(response, 'bar@example.com') diff --git a/password_reset/views.py b/password_reset/views.py index 88b84a07..0dccc536 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib.sites.models import Site, RequestSite from django.core import signing -from django.core.mail import send_mail +from django.core.mail import EmailMultiAlternatives from django.core.urlresolvers import reverse, reverse_lazy from django.shortcuts import get_object_or_404, redirect from django.http import Http404 @@ -53,6 +53,7 @@ class Recover(SaltMixin, generic.FormView): template_name = 'password_reset/recovery_form.html' success_url_name = 'password_reset_sent' email_template_name = 'password_reset/recovery_email.txt' + email_html_template_name = 'password_reset/recovery_email.html' email_subject_template_name = 'password_reset/recovery_email_subject.txt' search_fields = ['username', 'email'] @@ -85,12 +86,14 @@ def send_notification(self): 'token': signing.dumps(self.user.pk, salt=self.salt), 'secure': self.request.is_secure(), } - body = loader.render_to_string(self.email_template_name, - context).strip() - subject = loader.render_to_string(self.email_subject_template_name, - context).strip() - send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, - [self.user.email]) + text_content = loader.render_to_string(self.email_template_name, context).strip() + html_content = loader.render_to_string(self.email_html_template_name, context).strip() + subject = loader.render_to_string(self.email_subject_template_name, context).strip() + + msg = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [self.user.email, ]) + msg.attach_alternative(html_content, 'text/html') + + msg.send() def form_valid(self, form): self.user = form.cleaned_data['user'] From 9af82f9df0638769902e0db5388084e81180236d Mon Sep 17 00:00:00 2001 From: ellmetha Date: Sun, 26 Oct 2014 15:33:09 +0100 Subject: [PATCH 02/34] Fixed PEP8 compliance (E501) --- password_reset/views.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/password_reset/views.py b/password_reset/views.py index 0dccc536..4f56f77e 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -86,11 +86,16 @@ def send_notification(self): 'token': signing.dumps(self.user.pk, salt=self.salt), 'secure': self.request.is_secure(), } - text_content = loader.render_to_string(self.email_template_name, context).strip() - html_content = loader.render_to_string(self.email_html_template_name, context).strip() - subject = loader.render_to_string(self.email_subject_template_name, context).strip() - - msg = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [self.user.email, ]) + text_content = loader.render_to_string( + self.email_template_name, context).strip() + html_content = loader.render_to_string( + self.email_html_template_name, context).strip() + subject = loader.render_to_string( + self.email_subject_template_name, context).strip() + + msg = EmailMultiAlternatives( + subject, text_content, + settings.DEFAULT_FROM_EMAIL, [self.user.email, ]) msg.attach_alternative(html_content, 'text/html') msg.send() From 73fb549d22a259428713960be2043df764ab1189 Mon Sep 17 00:00:00 2001 From: ellmetha Date: Tue, 18 Nov 2014 01:01:40 +0100 Subject: [PATCH 03/34] Base email template updated to properly handle secure http --- password_reset/templates/password_reset/base_email.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/templates/password_reset/base_email.html b/password_reset/templates/password_reset/base_email.html index da53587a..09b7ca8d 100644 --- a/password_reset/templates/password_reset/base_email.html +++ b/password_reset/templates/password_reset/base_email.html @@ -172,7 +172,7 @@ - {% blocktrans with site_name=site.name %}Go to {{ site_name }}{% endblocktrans %} + {% blocktrans with site_name=site.name %}Go to {{ site_name }}{% endblocktrans %} From 342ac5075fd303eaef057f1d6c28c40788c07f8f Mon Sep 17 00:00:00 2001 From: Ezequiel Golub Date: Fri, 10 Apr 2015 00:55:35 -0300 Subject: [PATCH 04/34] Updated spanish translation Redundant "minutos" removed from spanish translation --- password_reset/locale/es/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/locale/es/LC_MESSAGES/django.po b/password_reset/locale/es/LC_MESSAGES/django.po index a5423683..df7c8e2d 100644 --- a/password_reset/locale/es/LC_MESSAGES/django.po +++ b/password_reset/locale/es/LC_MESSAGES/django.po @@ -127,5 +127,5 @@ msgid "" "An email was sent to %(email)s %(ago)s ago. Use the link in " "it to set a new password." msgstr "" -"Un email ha sido enviado a %(email)s hace %(ago)s minutos. " +"Un email ha sido enviado a %(email)s hace %(ago)s. " "Use el enlace recibido para recuperar su clave" From bfae6ddb629837dca9e18c8db0f765832ad25907 Mon Sep 17 00:00:00 2001 From: sketchytechky Date: Wed, 15 Apr 2015 13:32:50 -0700 Subject: [PATCH 05/34] Generating link both from user.pk and user.password to mitigate issue with permanent link exposed to gain access --- password_reset/views.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/password_reset/views.py b/password_reset/views.py index 88b84a07..2ad71365 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -10,6 +10,7 @@ from django.template import loader from django.utils import timezone from django.views import generic +import hashlib from .forms import PasswordRecoveryForm, PasswordResetForm from .utils import get_user_model, get_username @@ -82,7 +83,13 @@ def send_notification(self): 'site': self.get_site(), 'user': self.user, 'username': get_username(self.user), - 'token': signing.dumps(self.user.pk, salt=self.salt), + 'token': signing.dumps({ + 'pk': self.user.pk, + 'psw': hashlib.sha256( + self.user.password + ) + }, + salt=self.salt), 'secure': self.request.is_secure(), } body = loader.render_to_string(self.email_template_name, @@ -121,12 +128,25 @@ def dispatch(self, request, *args, **kwargs): self.kwargs = kwargs try: - pk = signing.loads(kwargs['token'], max_age=self.token_expires, - salt=self.salt) + unsigned_pk_hash = signing.loads(kwargs['token'], + max_age=self.token_expires, + salt=self.salt) except signing.BadSignature: return self.invalid() + try: + pk = unsigned_pk_hash['pk'] + password = unsigned_pk_hash['psw'] + except KeyError: + return self.invalid() + self.user = get_object_or_404(get_user_model(), pk=pk) + + # Ensure the hashed password is same to prevent link to be reused + # TODO: this is assuming the password is changed + if password != hashlib.sha256(self.user.password): + return self.invalid() + return super(Reset, self).dispatch(request, *args, **kwargs) def invalid(self): From 46d6f2c4ebfbdda27954cfbffd4be14b92708fcf Mon Sep 17 00:00:00 2001 From: sketchytechky Date: Wed, 15 Apr 2015 16:29:03 -0700 Subject: [PATCH 06/34] Tidy up password hashing call --- password_reset/views.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/password_reset/views.py b/password_reset/views.py index 2ad71365..e9077642 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -21,6 +21,9 @@ class SaltMixin(object): salt = 'password_recovery' url_salt = 'password_recovery_url' + def hash_password(self, psw): + return hashlib.sha512(psw).hexdigest() + def loads_with_timestamp(value, salt): """Returns the unsigned value along with its timestamp, the time when it @@ -83,13 +86,14 @@ def send_notification(self): 'site': self.get_site(), 'user': self.user, 'username': get_username(self.user), - 'token': signing.dumps({ - 'pk': self.user.pk, - 'psw': hashlib.sha256( - self.user.password - ) - }, - salt=self.salt), + 'token': signing.dumps( + { + 'pk': self.user.pk, + 'psw': self.hash_password( + self.user.password + ) + }, + salt=self.salt), 'secure': self.request.is_secure(), } body = loader.render_to_string(self.email_template_name, @@ -144,7 +148,7 @@ def dispatch(self, request, *args, **kwargs): # Ensure the hashed password is same to prevent link to be reused # TODO: this is assuming the password is changed - if password != hashlib.sha256(self.user.password): + if password != self.hash_password(self.user.password): return self.invalid() return super(Reset, self).dispatch(request, *args, **kwargs) From b4e45c9047363bcd9391e71c19867e55462254b7 Mon Sep 17 00:00:00 2001 From: sketchytechky Date: Wed, 15 Apr 2015 16:50:32 -0700 Subject: [PATCH 07/34] Use django hasher to avoid handling hashlib different across py27 and py33 --- password_reset/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/password_reset/views.py b/password_reset/views.py index e9077642..db85d587 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -10,7 +10,7 @@ from django.template import loader from django.utils import timezone from django.views import generic -import hashlib +from django.contrib.auth.hashers import get_hasher from .forms import PasswordRecoveryForm, PasswordResetForm from .utils import get_user_model, get_username @@ -22,7 +22,7 @@ class SaltMixin(object): url_salt = 'password_recovery_url' def hash_password(self, psw): - return hashlib.sha512(psw).hexdigest() + return get_hasher().encode(psw, self.salt) def loads_with_timestamp(value, salt): From 62aa3ab607e39726aa983711c1b2aff9489d0b84 Mon Sep 17 00:00:00 2001 From: Paul Nurkkala Date: Mon, 18 Jan 2016 16:07:22 -0500 Subject: [PATCH 08/34] adding the ability to change case sensitivity with a django setting --- password_reset/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/password_reset/views.py b/password_reset/views.py index 6be66ee0..6f3f1259 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -71,6 +71,10 @@ def get_context_data(self, **kwargs): def get_form_kwargs(self): kwargs = super(Recover, self).get_form_kwargs() + + if hasattr(settings, 'PASSWORD_RESET_CASE_SENSITIVE'): + self.case_sensitive = settings.PASSWORD_RESET_CASE_SENSITIVE + kwargs.update({ 'case_sensitive': self.case_sensitive, 'search_fields': self.search_fields, From 762a9692314cb9d75e4b753948171ff7b25f91f8 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Thu, 24 Mar 2016 15:12:30 +0800 Subject: [PATCH 09/34] added logic to show an error page/msg if email cannot be sent as reported by email smtp server. Also added optional captcha in reset page if django-simple-captcha==0.5.1 is installed --- docs/quickstart.rst | 2 + password_reset/__init__.py | 2 +- password_reset/forms.py | 10 ++ .../locale/zh-hans/LC_MESSAGES/django.mo | Bin 0 -> 3212 bytes .../locale/zh-hans/LC_MESSAGES/django.po | 160 ++++++++++++++++++ .../zh-hans/LC_MESSAGES/django.po.original | 122 +++++++++++++ password_reset/locale/zh_Hans | 1 + .../password_reset/recovery_email.txt | 4 + .../password_reset/recovery_form.html | 46 ++++- .../templates/password_reset/reset_sent.html | 6 + .../password_reset/reset_sent_failed.html | 11 ++ password_reset/urls.py | 2 + password_reset/views.py | 32 +++- setup.py | 1 + 14 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 password_reset/locale/zh-hans/LC_MESSAGES/django.mo create mode 100644 password_reset/locale/zh-hans/LC_MESSAGES/django.po create mode 100644 password_reset/locale/zh-hans/LC_MESSAGES/django.po.original create mode 120000 password_reset/locale/zh_Hans create mode 100644 password_reset/templates/password_reset/reset_sent_failed.html diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2776bd50..42482bf4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -32,6 +32,8 @@ What you get * Password reset links that expire in two days (configurable). +* Optionally invoke a captcha in the password reset page if django-simple-captcha (0.5.1 or higher) is installed and configured. + What you can do --------------- diff --git a/password_reset/__init__.py b/password_reset/__init__.py index ef72cc0f..4ca39e7c 100644 --- a/password_reset/__init__.py +++ b/password_reset/__init__.py @@ -1 +1 @@ -__version__ = '0.8.1' +__version__ = '0.8.2' diff --git a/password_reset/forms.py b/password_reset/forms.py index 4f8b4ec5..1c8f16b9 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -4,11 +4,21 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings + +try: + from captcha.fields import CaptchaField # uses django-simple-captcha==0.5.1 +except ImportError: + pass + from .utils import get_user_model class PasswordRecoveryForm(forms.Form): username_or_email = forms.CharField() + try: + captcha = CaptchaField(label=_('Captcha')) # Optional Captcha + except: + pass error_messages = { 'not_found': _("Sorry, this user doesn't exist."), diff --git a/password_reset/locale/zh-hans/LC_MESSAGES/django.mo b/password_reset/locale/zh-hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..eb97a3849505d7ac3deda98f3749d92d32ec2515 GIT binary patch literal 3212 zcmai#TWlQF8OINoLTm1nYfIrMQot6TjguA>7UO1XFTui>$X*A9R;AfeQ0-PcM(z^dEtToH)rmIkW$H^zIpk<2jtq;C$>3#u~u;@5BYi3itrH3J!pq;Jx7UcQN)T7y^F* zM!;5ZFZfsRICww!4fq839r!T#;N6Tp0zLh5 zufcc0d*Phc+u$+qOz`~dJ%|O*E#NP~UXc9$1$+xM!I!{)f&0Pl!TZ2o1h)q{cnACy zp69UmF!&ir&pU8=3Va2`64nDozyn|t_$i1V`wExG!GD7ff!~6ZukQmsRHy8018MwK zkn+_P@XsKovO$p6OOWCo1OEnI1%CrR3DIwZyTE6`Ns#)aH>d`P$L zVM``LtW_U!;)6PC6S{DWSt*e9*Fhtd)`f z=cIjrMtQz%9!h1oku)tyAsBX$1>HO%Au9}FpJn;IsWeylJ-LQ2Sj@iB-1zd`_bx_Fc?IXOaJI3uyJT7c|Ad~u$36_bR zB`hS$Y_NaiJ=Kl%Cuah=i{gh6FDWW{c%yv9#uInS6(xmVp`!`?wXWh6rU0Ez@_NHC?jS?7aN47|2& zdwn@a?_^}E!QR#)#d{fHm%GU_Me`25WvegJhKdWD#EJLO*C7@|ORAnnusIl@`bdACkfm zX2LL&WCcoT3t-A1_H&Rw1g%#T1W|5jsSvA_q`X&Z8-n*|5m+i^4f|ebw@^*Pw5r+z zzQ<}CV|(=rVJdgknj=EmVA4nqqU)?-4k7+0#7d$chtQ9{81!3AW>VNK()zoIJ6d}} z+m9|pQNny*r1j0n-YDN6?dfgr?yBn%LzZ;3PCIENv_EB%widI(yl!82Oly_GucX$d zJ0i??HSK&^YucqXy~KCE9R9=Z7n^oBHDRLGBMus5JeYSN8tKtGyS26mf1^9x+5Y0& z-CfbT4&8RNnAA->Mc>Xaf4#4xgLg$bqr5rV8ENm>6KQMfiT3ukV5*tSAQ&ws^mLea zM7s9&k>uu%jy)+OWXWW84y1OrcSfu6?F=>5wW7tatF>5m2)Z5dzO!Q}g)RFkzJI#J zwtB5E_PVyaI*()x2&qNQxJBI%=DYh1r;ZhJ^M$D+#gPw~d+i^E^;7QIrQjQ$Ute@5 zCf%vi-q=!M_Hbz|SIDi_e1vcN_PeJ}mqteO*Ni* zEL<-go%B9jDdsi`H*?A~9^K_5g}KoXE9Mr8%cHnuwf_Pzg_-W+&09BSnG(Xv11GBg zMzlTD-$u>f0yGpi&bS{e-tO!8Vt#GXJ9Gt#AV*1nEBDkg#lCs7Ftg}gUQpWqH!{X5 zI;(fLZj2Y^b5MQj#stLsF_oiXwUH|8Oy#K7Ajc zuHxKzcWuo(w(4D;Ky;;Z=e^I@i`NbnrkA|&Yu-n5-jx}cLN%0KKhnzVGaKH?#h^77 zR=t_=pcX1eXP18J`%L9u9m}tc8nbnlAPb1_ePf?*}HVHxOuWTI$gN3?v0<$uU|sXy19$q*~!wO zrP4=B6jpw1!MilC+JRpCO$XPISUmbU^CmV($M}kO;;5UORVr9TWA)A({}4N-+K71e zE==XGUq=A7g)B^8K@PAZY9;&mMK}8yERD<-H;<51Yy4C;y literal 0 HcmV?d00001 diff --git a/password_reset/locale/zh-hans/LC_MESSAGES/django.po b/password_reset/locale/zh-hans/LC_MESSAGES/django.po new file mode 100644 index 00000000..6fdbb8df --- /dev/null +++ b/password_reset/locale/zh-hans/LC_MESSAGES/django.po @@ -0,0 +1,160 @@ +# 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-02-05 16:48+0800\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" + +#: forms.py:14 +msgid "Captcha" +msgstr "认证码" + +#: forms.py:17 +msgid "Sorry, this user doesn't exist." +msgstr "抱歉,用户名不存在." + +#: forms.py:34 +msgid "Username" +msgstr "用户名" + +#: forms.py:35 +msgid "Email" +msgstr "邮箱" + +#: forms.py:36 +msgid "Username or Email" +msgstr "用户名或者邮箱" + +#: forms.py:58 +msgid "Sorry, inactive users can't recover their password." +msgstr "非常抱歉,非活跃用户无法重置密码。" + +#: forms.py:96 +msgid "Unable to find user." +msgstr "找不到指定用户" + +#: forms.py:103 +msgid "New password" +msgstr "新密码" + +#: forms.py:107 +msgid "New password (confirm)" +msgstr "新密码(确认)" + +#: forms.py:112 +msgid "The two passwords didn't match." +msgstr "两次输入的密码不一致" + +#: templates/password_reset/recovery_done.html:3 +msgid "New password set" +msgstr "设置新密码" + +#: templates/password_reset/recovery_done.html:6 +msgid "" +"Your password has successfully been reset. You can use it right now on the " +"login page." +msgstr "您的密码已经重设成功,现在可以登录了。" + +#: templates/password_reset/recovery_email.txt:1 +#, python-format +msgid "Dear %(username)s," +msgstr "%(username)s,您好:" + +#: templates/password_reset/recovery_email.txt:3 +#, python-format +msgid "" +"You -- or someone pretending to be you -- has requested a password reset on " +"%(domain)s." +msgstr "您或某人在 %(domain)s 申请了密码重置。" + +#: templates/password_reset/recovery_email.txt:5 +msgid "You can set your new password by following this link:" +msgstr "您可以通过以下链接重置密码:" + +#: templates/password_reset/recovery_email.txt:9 +msgid "" +"If you don't want to reset your password, simply ignore this email and it " +"will stay unchanged." +msgstr "若不想重设密码,可以忽略此邮件。" + +#: templates/password_reset/recovery_email.txt:11 +msgid "Yours Truly" +msgstr "感谢您的惠顾" + +#: templates/password_reset/recovery_email.txt:12 +msgid "Administrator" +msgstr "管理者" + +#: templates/password_reset/recovery_email_subject.txt:1 +#, python-format +msgid "Password recovery on %(domain)s" +msgstr "重设 %(domain)s 密码" + +#: templates/password_reset/recovery_form.html:5 +msgid "Password recovery" +msgstr "重新设置密码" + +#: templates/password_reset/recovery_form.html:8 +msgid "Recover Password" +msgstr "重设密码" + +#: templates/password_reset/recovery_form.html:60 +msgid "Recover my password" +msgstr "重设密码" + +#: templates/password_reset/reset.html:5 +#, python-format +msgid "" +"Sorry, this password reset link is invalid. You can still request a new one." +msgstr "" +"抱歉,链接已经失效,您可以在 请求一个新链接." + +#: templates/password_reset/reset.html:7 +#, python-format +msgid "Hi, %(username)s. Please choose your new password." +msgstr "您好,%(username)s. 请输入您的新密码." + +#: templates/password_reset/reset.html:11 +msgid "Set new password" +msgstr "设置新密码" + +#: templates/password_reset/reset_sent.html:4 +msgid "Password recovery sent" +msgstr "发送重设密码" + +#: templates/password_reset/reset_sent.html:8 +#, python-format +msgid "" +"An email was sent to %(email)s %(ago)s ago. Use the link in " +"it to set a new password." +msgstr "" +"已经在%(ago)s以前向您的邮箱 %(email)s 发送了邮件,请根据邮件" +"提示重新设置您的新密码." + +#: templates/password_reset/reset_sent.html:11 +#: templates/password_reset/reset_sent_failed.html:9 +msgid "Return to Main Page" +msgstr "回主页" + +#: templates/password_reset/reset_sent_failed.html:4 +msgid "Password recovery NOT successful" +msgstr "发送重设密码" + +#: templates/password_reset/reset_sent_failed.html:7 +msgid "" +"The email for this registered user is invalid, password reset information " +"cannot be sent. Please contact our administrator for assistance." +msgstr "此用户电邮地址无法接收密码重置信息。欢迎联系我们的客服部门。" diff --git a/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original b/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original new file mode 100644 index 00000000..12fa1c0a --- /dev/null +++ b/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original @@ -0,0 +1,122 @@ +# 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: 2014-01-23 10:01+0800\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" + +#: forms.py:26 +msgid "Username" +msgstr "用户名" + +#: forms.py:27 +msgid "Email" +msgstr "邮箱" + +#: forms.py:28 +msgid "Username or Email" +msgstr "用户名或者邮箱" + +#: forms.py:48 forms.py:58 forms.py:70 +msgid "Sorry, this user doesn't exist." +msgstr "抱歉,用户名不存在." + +#: forms.py:72 +msgid "Unable to find user." +msgstr "找不到指定用户" + +#: forms.py:78 +msgid "New password" +msgstr "新密码" + +#: forms.py:82 +msgid "New password (confirm)" +msgstr "新密码(确认)" + +#: forms.py:94 +msgid "The two passwords didn't match." +msgstr "两次输入的密码不一致" + +#: templates/password_reset/recovery_done.html:3 +msgid "New password set" +msgstr "设置新密码" + +#: templates/password_reset/recovery_done.html:6 +msgid "" +"Your password has successfully been reset. You can use it right now on the " +"login page." +msgstr "您的密码已经重设成功,现在可以登录了." + +#: templates/password_reset/recovery_email.txt:1 +#, python-format +msgid "Dear %(username)s," +msgstr "您好 %(username)s," + +#: templates/password_reset/recovery_email.txt:3 +#, python-format +msgid "" +"You -- or someone pretending to be you -- has requested a password reset on " +"%(domain)s." +msgstr "您或者其它的人在 %(domain)s 申请了密码重置." + +#: templates/password_reset/recovery_email.txt:5 +msgid "You can set your new password by following this link:" +msgstr "您可以通过以下链接重置密码:" + +#: templates/password_reset/recovery_email.txt:9 +msgid "" +"If you don't want to reset your password, simply ignore this email and it " +"will stay unchanged." +msgstr "如果您不想重设密码,可以忽略此邮箱." + +#: templates/password_reset/recovery_email_subject.txt:1 +#, python-format +msgid "Password recovery on %(domain)s" +msgstr "在 %(domain)s 上重设密码" + +#: templates/password_reset/recovery_form.html:5 +msgid "Password recovery" +msgstr "重新设置密码" + +#: templates/password_reset/recovery_form.html:11 +msgid "Recover my password" +msgstr "重设密码" + +#: templates/password_reset/reset.html:5 +#, python-format +msgid "" +"Sorry, this password reset link is invalid. You can still request a new one." +msgstr "抱歉,链接已经失效,您可以在 请求一个新链接." + +#: templates/password_reset/reset.html:7 +#, python-format +msgid "Hi, %(username)s. Please choose your new password." +msgstr "您好,%(username)s. 请输入您的新密码." + +#: templates/password_reset/reset.html:11 +msgid "Set new password" +msgstr "设置新密码" + +#: templates/password_reset/reset_sent.html:4 +msgid "Password recovery sent" +msgstr "发送重设密码" + +#: templates/password_reset/reset_sent.html:7 +#, python-format +msgid "" +"An email was sent to %(email)s %(ago)s ago. Use the link in " +"it to set a new password." +msgstr "已经在%(ago)s以前向您的邮箱 %(email)s发送了邮件,根据邮件提示重新设置您的新密码." diff --git a/password_reset/locale/zh_Hans b/password_reset/locale/zh_Hans new file mode 120000 index 00000000..80d1c227 --- /dev/null +++ b/password_reset/locale/zh_Hans @@ -0,0 +1 @@ +zh-hans \ No newline at end of file diff --git a/password_reset/templates/password_reset/recovery_email.txt b/password_reset/templates/password_reset/recovery_email.txt index 9c4af9cc..70b1af2a 100644 --- a/password_reset/templates/password_reset/recovery_email.txt +++ b/password_reset/templates/password_reset/recovery_email.txt @@ -7,3 +7,7 @@ http{% if secure %}s{% endif %}://{{ site.domain }}{% url "password_reset_reset" token %} {% trans "If you don't want to reset your password, simply ignore this email and it will stay unchanged." %} + +{% trans "Yours Truly" %}, +{% trans "Administrator" %} +{{ site.domain }} diff --git a/password_reset/templates/password_reset/recovery_form.html b/password_reset/templates/password_reset/recovery_form.html index e0aff0ca..defaca11 100644 --- a/password_reset/templates/password_reset/recovery_form.html +++ b/password_reset/templates/password_reset/recovery_form.html @@ -1,12 +1,46 @@ {% extends "password_reset/base.html" %} {% load i18n %} +{% load sekizai_tags %} {% block title %}{% trans "Password recovery" %}{% endblock %} {% block content %} -
- {% csrf_token %} - {{ form.as_p }} -

-
-{% endblock %} +

{% trans "Recover Password" %}

+
+{% csrf_token %} +{% for field in form %} +{% if field.name != "captcha" %} +
+ {{ field.label_tag }} + +
+{% else %} +

+

+ + + +   {{ field }} +
+{% endif %} +
+ {{ field.errors }} +
+{% endfor %} + +
+
+ +
+
+ +
+{% endblock content %} diff --git a/password_reset/templates/password_reset/reset_sent.html b/password_reset/templates/password_reset/reset_sent.html index bc56b7c2..f5362f78 100644 --- a/password_reset/templates/password_reset/reset_sent.html +++ b/password_reset/templates/password_reset/reset_sent.html @@ -4,5 +4,11 @@ {% block title %}{% trans "Password recovery sent" %}{% endblock %} {% block content %} + + +{% trans "Return to Main Page" %} + + {% endblock %} diff --git a/password_reset/templates/password_reset/reset_sent_failed.html b/password_reset/templates/password_reset/reset_sent_failed.html new file mode 100644 index 00000000..77f3f6c3 --- /dev/null +++ b/password_reset/templates/password_reset/reset_sent_failed.html @@ -0,0 +1,11 @@ +{% extends "password_reset/base.html" %} +{% load i18n %} + +{% block title %}{% trans "Password recovery NOT successful" %}{% endblock %} + +{% block content %} +

{% trans "The email for this registered user is invalid, password reset information cannot be sent. Please contact our administrator for assistance." %}

+ +{% trans "Return to Main Page" %} + +{% endblock %} diff --git a/password_reset/urls.py b/password_reset/urls.py index eedad198..7b4817d9 100644 --- a/password_reset/urls.py +++ b/password_reset/urls.py @@ -6,6 +6,8 @@ urlpatterns = [ url(r'^recover/(?P.+)/$', views.recover_done, name='password_reset_sent'), + url(r'^recover/(?P.+)/$', views.recover_failed, + name='password_reset_sent_failed'), url(r'^recover/$', views.recover, name='password_reset_recover'), url(r'^reset/done/$', views.reset_done, name='password_reset_done'), url(r'^reset/(?P[\w:-]+)/$', views.reset, diff --git a/password_reset/views.py b/password_reset/views.py index 6be66ee0..6cb35a17 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -31,7 +31,7 @@ def loads_with_timestamp(value, salt): """Returns the unsigned value along with its timestamp, the time when it got dumped.""" try: - signing.loads(value, salt=salt, max_age=-1) + signing.loads(value, salt=salt, max_age=-999999) except signing.SignatureExpired as e: age = float(str(e).split('Signature age ')[1].split(' >')[0]) timestamp = timezone.now() - datetime.timedelta(seconds=age) @@ -52,18 +52,39 @@ def get_context_data(self, **kwargs): return ctx recover_done = RecoverDone.as_view() +class RecoverFailed(SaltMixin, generic.TemplateView): + template_name = 'password_reset/reset_sent_failed.html' + + def get_context_data(self, **kwargs): + ctx = super(RecoverFailed, self).get_context_data(**kwargs) + try: + ctx['timestamp'], ctx['email'] = loads_with_timestamp( + self.kwargs['signature'], salt=self.url_salt, + ) + except signing.BadSignature: + raise Http404 + return ctx +recover_failed = RecoverFailed.as_view() + + class Recover(SaltMixin, generic.FormView): case_sensitive = True form_class = PasswordRecoveryForm template_name = 'password_reset/recovery_form.html' success_url_name = 'password_reset_sent' + failure_url_name = 'password_reset_sent_failed' email_template_name = 'password_reset/recovery_email.txt' email_subject_template_name = 'password_reset/recovery_email_subject.txt' search_fields = ['username', 'email'] + sent_success = 0 def get_success_url(self): - return reverse(self.success_url_name, args=[self.mail_signature]) + if self.sent_success: + return reverse(self.success_url_name, args=[self.mail_signature]) + else: + return reverse(self.failure_url_name, args=[self.mail_signature]) + def get_context_data(self, **kwargs): kwargs['url'] = self.request.get_full_path() @@ -92,12 +113,13 @@ def send_notification(self): context).strip() subject = loader.render_to_string(self.email_subject_template_name, context).strip() - send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, - [self.user.email]) + result = send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, + [self.user.email], fail_silently=True) + return result def form_valid(self, form): self.user = form.cleaned_data['user'] - self.send_notification() + self.sent_success = self.send_notification() if ( len(self.search_fields) == 1 and self.search_fields[0] == 'username' diff --git a/setup.py b/setup.py index df82b271..b657ef5f 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ long_description=open('README.rst').read(), install_requires=[ 'Django>=1.4', + #'django-simple-captcha>=0.5.1', # will use captcha if installed ], classifiers=[ 'Development Status :: 4 - Beta', From ad9b1fec09eddd8252fba4a64ad898fa9fa8c5af Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Thu, 24 Mar 2016 15:35:55 +0800 Subject: [PATCH 10/34] removed/cleaned django.po.original --- .../zh-hans/LC_MESSAGES/django.po.original | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 password_reset/locale/zh-hans/LC_MESSAGES/django.po.original diff --git a/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original b/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original deleted file mode 100644 index 12fa1c0a..00000000 --- a/password_reset/locale/zh-hans/LC_MESSAGES/django.po.original +++ /dev/null @@ -1,122 +0,0 @@ -# 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: 2014-01-23 10:01+0800\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" - -#: forms.py:26 -msgid "Username" -msgstr "用户名" - -#: forms.py:27 -msgid "Email" -msgstr "邮箱" - -#: forms.py:28 -msgid "Username or Email" -msgstr "用户名或者邮箱" - -#: forms.py:48 forms.py:58 forms.py:70 -msgid "Sorry, this user doesn't exist." -msgstr "抱歉,用户名不存在." - -#: forms.py:72 -msgid "Unable to find user." -msgstr "找不到指定用户" - -#: forms.py:78 -msgid "New password" -msgstr "新密码" - -#: forms.py:82 -msgid "New password (confirm)" -msgstr "新密码(确认)" - -#: forms.py:94 -msgid "The two passwords didn't match." -msgstr "两次输入的密码不一致" - -#: templates/password_reset/recovery_done.html:3 -msgid "New password set" -msgstr "设置新密码" - -#: templates/password_reset/recovery_done.html:6 -msgid "" -"Your password has successfully been reset. You can use it right now on the " -"login page." -msgstr "您的密码已经重设成功,现在可以登录了." - -#: templates/password_reset/recovery_email.txt:1 -#, python-format -msgid "Dear %(username)s," -msgstr "您好 %(username)s," - -#: templates/password_reset/recovery_email.txt:3 -#, python-format -msgid "" -"You -- or someone pretending to be you -- has requested a password reset on " -"%(domain)s." -msgstr "您或者其它的人在 %(domain)s 申请了密码重置." - -#: templates/password_reset/recovery_email.txt:5 -msgid "You can set your new password by following this link:" -msgstr "您可以通过以下链接重置密码:" - -#: templates/password_reset/recovery_email.txt:9 -msgid "" -"If you don't want to reset your password, simply ignore this email and it " -"will stay unchanged." -msgstr "如果您不想重设密码,可以忽略此邮箱." - -#: templates/password_reset/recovery_email_subject.txt:1 -#, python-format -msgid "Password recovery on %(domain)s" -msgstr "在 %(domain)s 上重设密码" - -#: templates/password_reset/recovery_form.html:5 -msgid "Password recovery" -msgstr "重新设置密码" - -#: templates/password_reset/recovery_form.html:11 -msgid "Recover my password" -msgstr "重设密码" - -#: templates/password_reset/reset.html:5 -#, python-format -msgid "" -"Sorry, this password reset link is invalid. You can still request a new one." -msgstr "抱歉,链接已经失效,您可以在 请求一个新链接." - -#: templates/password_reset/reset.html:7 -#, python-format -msgid "Hi, %(username)s. Please choose your new password." -msgstr "您好,%(username)s. 请输入您的新密码." - -#: templates/password_reset/reset.html:11 -msgid "Set new password" -msgstr "设置新密码" - -#: templates/password_reset/reset_sent.html:4 -msgid "Password recovery sent" -msgstr "发送重设密码" - -#: templates/password_reset/reset_sent.html:7 -#, python-format -msgid "" -"An email was sent to %(email)s %(ago)s ago. Use the link in " -"it to set a new password." -msgstr "已经在%(ago)s以前向您的邮箱 %(email)s发送了邮件,根据邮件提示重新设置您的新密码." From 15eb17f830d45b6db9ce754195cfc88f04928e5a Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 09:44:36 +0800 Subject: [PATCH 11/34] removed sekizai_tags from include, which broke the travis build --- password_reset/templates/password_reset/recovery_form.html | 1 - 1 file changed, 1 deletion(-) diff --git a/password_reset/templates/password_reset/recovery_form.html b/password_reset/templates/password_reset/recovery_form.html index defaca11..4e152c0f 100644 --- a/password_reset/templates/password_reset/recovery_form.html +++ b/password_reset/templates/password_reset/recovery_form.html @@ -1,6 +1,5 @@ {% extends "password_reset/base.html" %} {% load i18n %} -{% load sekizai_tags %} {% block title %}{% trans "Password recovery" %}{% endblock %} From 22567e8e3d479421766699ce9a7cd2685e5d18d5 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 17:09:35 +0800 Subject: [PATCH 12/34] fixed test cases to work with optional captchas, also unified error messages in forms --- password_reset/forms.py | 25 +++++++++++-------------- password_reset/tests/settings.py | 14 ++++++++++++-- password_reset/tests/tests.py | 23 +++++++++++++++++------ password_reset/tests/urls.py | 12 +++++++++++- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index 1c8f16b9..1eb9a5ec 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -8,22 +8,23 @@ try: from captcha.fields import CaptchaField # uses django-simple-captcha==0.5.1 except ImportError: + CaptchaField = None pass from .utils import get_user_model +error_messages = { + 'not_found': _("Sorry, this user doesn't exist."), + 'password_mismatch':_("The two passwords didn't match."), +} class PasswordRecoveryForm(forms.Form): username_or_email = forms.CharField() try: - captcha = CaptchaField(label=_('Captcha')) # Optional Captcha + if CaptchaField and settings.CAPTCHA_CHALLENGE_FUNCT: # defined + captcha = CaptchaField(label=_('Captcha')) # Optional Captcha except: pass - - error_messages = { - 'not_found': _("Sorry, this user doesn't exist."), - } - def __init__(self, *args, **kwargs): self.case_sensitive = kwargs.pop('case_sensitive', True) search_fields = kwargs.pop('search_fields', ('username', 'email')) @@ -73,7 +74,7 @@ def get_user_by_username(self, username): try: user = User._default_manager.get(**{key: username}) except User.DoesNotExist: - raise forms.ValidationError(self.error_messages['not_found'], + raise forms.ValidationError(error_messages['not_found'], code='not_found') return user @@ -84,7 +85,7 @@ def get_user_by_email(self, email): try: user = User._default_manager.get(**{key: email}) except User.DoesNotExist: - raise forms.ValidationError(self.error_messages['not_found'], + raise forms.ValidationError(error_messages['not_found'], code='not_found') return user @@ -97,7 +98,7 @@ def get_user_by_both(self, username): try: user = User._default_manager.get(filters) except User.DoesNotExist: - raise forms.ValidationError(self.error_messages['not_found'], + raise forms.ValidationError(error_messages['not_found'], code='not_found') except User.MultipleObjectsReturned: raise forms.ValidationError(_("Unable to find user.")) @@ -115,10 +116,6 @@ class PasswordResetForm(forms.Form): widget=forms.PasswordInput, ) - error_messages = { - 'password_mismatch': _("The two passwords didn't match."), - } - def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') super(PasswordResetForm, self).__init__(*args, **kwargs) @@ -128,7 +125,7 @@ def clean_password2(self): password2 = self.cleaned_data['password2'] if not password1 == password2: raise forms.ValidationError( - self.error_messages['password_mismatch'], + error_messages['password_mismatch'], code='password_mismatch') return password2 diff --git a/password_reset/tests/settings.py b/password_reset/tests/settings.py index f7c1479b..f9c72830 100644 --- a/password_reset/tests/settings.py +++ b/password_reset/tests/settings.py @@ -9,12 +9,19 @@ }, } -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'password_reset', 'password_reset.tests', -) +] + +try: + INSTALLED_APPS += [ + #'captcha' # include captcha if available + ] +except: + pass MIGRATION_MODULES = { 'auth': 'django.contrib.auth.tests.migrations', @@ -28,3 +35,6 @@ ), }, }] + +#AUTH_USER_MODEL = None +DISABLE_CAPTCHA=True diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index b80b1dcd..51228c89 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -11,20 +11,30 @@ except ImportError: from unittest import SkipTest -from ..forms import PasswordRecoveryForm, PasswordResetForm +from ..forms import PasswordRecoveryForm, PasswordResetForm, error_messages from ..utils import get_user_model +#import settings +""" +Commented out this part due to incompatibilities with Django 1.8.4 +See http://stackoverflow.com/questions/27433228/django-1-7-django-db-utils-operationalerror-no-such-table-auth-customuser if django.VERSION >= (1, 5): from django.contrib.auth.tests.custom_user import ( # noqa CustomUser, ExtensionUser) else: CustomUser = None # noqa ExtensionUser = None # noqa +""" +CustomUser = None +ExtensionUser = None +# Test manually via +# ./manage.py test --settings password_reset.tests.settings password_reset.tests class CustomUserVariants(type): def __new__(cls, name, bases, dct): - if django.VERSION >= (1, 5): + #if django.VERSION >= (1, 5) and settings.AUTH_USER_MODEL: + if 1>2: for custom_user in ['auth.CustomUser', 'auth.ExtensionUser']: suffix = custom_user.lower().replace('.', '_') for key, fn in list(dct.items()): @@ -61,7 +71,7 @@ def test_username_input(self): form = PasswordRecoveryForm(data={'username_or_email': 'inexisting'}) self.assertFalse(form.is_valid()) self.assertEqual(form.errors['username_or_email'], - ["Sorry, this user doesn't exist."]) + [error_messages['not_found']]) create_user() @@ -133,7 +143,7 @@ def test_form_custom_search(self): }, search_fields=['email']) self.assertFalse(form.is_valid()) self.assertEqual(form.errors['username_or_email'], - ["Sorry, this user doesn't exist."]) + [error_messages['not_found']]) user = create_user() @@ -142,7 +152,7 @@ def test_form_custom_search(self): }, search_fields=['email']) self.assertFalse(form.is_valid()) self.assertEqual(form.errors['username_or_email'], - ["Sorry, this user doesn't exist."]) + [error_messages['not_found']]) # Search by actual email works form = PasswordRecoveryForm(data={ @@ -162,7 +172,7 @@ def test_form_custom_search(self): }, search_fields=['username']) self.assertFalse(form.is_valid()) self.assertEqual(form.errors['username_or_email'], - ["Sorry, this user doesn't exist."]) + [error_messages['not_found']]) form = PasswordRecoveryForm(data={ 'username_or_email': 'username', @@ -280,6 +290,7 @@ def test_email_recover(self): self.user = create_user() url = reverse('email_recover') response = self.client.get(url) + print "----------- guiyu: response=%s" % response self.assertNotContains(response, "Username or Email") self.assertContains(response, "Email:") diff --git a/password_reset/tests/urls.py b/password_reset/tests/urls.py index 25832f3d..edc07682 100644 --- a/password_reset/tests/urls.py +++ b/password_reset/tests/urls.py @@ -1,8 +1,13 @@ -from django.conf.urls import url +from django.conf.urls import url, include from ..urls import urlpatterns from . import views +try: + import captcha as captcha_installed +except: + captcha_installed = None + urlpatterns += [ url(r'^email_recover/$', views.email_recover, name='email_recover'), url(r'^username_recover/$', views.username_recover, @@ -10,3 +15,8 @@ url(r'^insensitive_recover/$', views.insensitive_recover, name='insensitive_recover'), ] + +if captcha_installed: + urlpatterns += [ + url(r'^captcha/', include('captcha.urls')), + ] From c1b9563dbf39e79853ddf79578fa7deccd9e8ed3 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 17:33:27 +0800 Subject: [PATCH 13/34] added blank models.py to work with Django 1.5 or lower; Added COLON_SUFFIX for django 1.5 tests to pass --- password_reset/models.py | 0 password_reset/tests/tests.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 password_reset/models.py diff --git a/password_reset/models.py b/password_reset/models.py new file mode 100644 index 00000000..e69de29b diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index 51228c89..b27956d5 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -28,6 +28,11 @@ CustomUser = None ExtensionUser = None + +if django.VERSION < (1, 6): + COLON_SUFFIX = '' # Django 1.5 or lower do NOT auto add colon suffix +else: + COLON_SUFFIX = ':' # Django 1.6 or higher auto add colon suffix # Test manually via # ./manage.py test --settings password_reset.tests.settings password_reset.tests @@ -290,9 +295,9 @@ def test_email_recover(self): self.user = create_user() url = reverse('email_recover') response = self.client.get(url) - print "----------- guiyu: response=%s" % response self.assertNotContains(response, "Username or Email") - self.assertContains(response, "Email:") + + self.assertContains(response, "Email%s" % COLON_SUFFIX) response = self.client.post(url, {'username_or_email': 'foo'}) try: @@ -319,7 +324,7 @@ def test_username_recover(self): response = self.client.get(url) self.assertNotContains(response, "Username or Email") - self.assertContains(response, "Username:") + self.assertContains(response, "Username%s" % COLON_SUFFIX) response = self.client.post(url, {'username_or_email': 'bar@example.com'}) From 074a35db1a8627397053531734f26b331696e1ee Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 17:59:31 +0800 Subject: [PATCH 14/34] fixed some lint errors --- password_reset/forms.py | 14 +++++++++----- password_reset/tests/settings.py | 10 ---------- password_reset/tests/tests.py | 1 - password_reset/utils.py | 6 ++++-- password_reset/views.py | 5 ++--- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index 1eb9a5ec..5ff771d1 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -6,7 +6,7 @@ try: - from captcha.fields import CaptchaField # uses django-simple-captcha==0.5.1 + from captcha.fields import CaptchaField # uses django-simple-captcha==0.5.1 except ImportError: CaptchaField = None pass @@ -14,17 +14,19 @@ from .utils import get_user_model error_messages = { - 'not_found': _("Sorry, this user doesn't exist."), - 'password_mismatch':_("The two passwords didn't match."), + 'not_found': _("Sorry, this user doesn't exist."), + 'password_mismatch': _("The two passwords didn't match."), } + class PasswordRecoveryForm(forms.Form): username_or_email = forms.CharField() try: - if CaptchaField and settings.CAPTCHA_CHALLENGE_FUNCT: # defined + if CaptchaField and settings.CAPTCHA_CHALLENGE_FUNCT: # defined captcha = CaptchaField(label=_('Captcha')) # Optional Captcha except: pass + def __init__(self, *args, **kwargs): self.case_sensitive = kwargs.pop('case_sensitive', True) search_fields = kwargs.pop('search_fields', ('username', 'email')) @@ -92,7 +94,9 @@ def get_user_by_email(self, email): def get_user_by_both(self, username): key = '__%sexact' key = key % '' if self.case_sensitive else key % 'i' - f = lambda field: Q(**{field + key: username}) + #f = lambda field: Q(**{field + key: username}) + def f(field): # to satisfy lint in Travis auto build on Github + return Q(**{field + key: username}) filters = f('username') | f('email') User = get_user_model() try: diff --git a/password_reset/tests/settings.py b/password_reset/tests/settings.py index f9c72830..22c1f6db 100644 --- a/password_reset/tests/settings.py +++ b/password_reset/tests/settings.py @@ -16,13 +16,6 @@ 'password_reset.tests', ] -try: - INSTALLED_APPS += [ - #'captcha' # include captcha if available - ] -except: - pass - MIGRATION_MODULES = { 'auth': 'django.contrib.auth.tests.migrations', } @@ -35,6 +28,3 @@ ), }, }] - -#AUTH_USER_MODEL = None -DISABLE_CAPTCHA=True diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index b27956d5..f07550c4 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -13,7 +13,6 @@ from ..forms import PasswordRecoveryForm, PasswordResetForm, error_messages from ..utils import get_user_model -#import settings """ Commented out this part due to incompatibilities with Django 1.8.4 diff --git a/password_reset/utils.py b/password_reset/utils.py index 6200970f..9721f5fe 100644 --- a/password_reset/utils.py +++ b/password_reset/utils.py @@ -2,8 +2,10 @@ from django.contrib.auth import get_user_model except ImportError: from django.contrib.auth.models import User - get_user_model = lambda: User # noqa - + #get_user_model = lambda: User # noqa + def my_user(): # to satisfy lint in Travis auto build on Github + return User # noqa + get_user_model = my_user def get_username(user): username_field = getattr(user, 'USERNAME_FIELD', 'username') diff --git a/password_reset/views.py b/password_reset/views.py index 6cb35a17..18ec50eb 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -52,6 +52,7 @@ def get_context_data(self, **kwargs): return ctx recover_done = RecoverDone.as_view() + class RecoverFailed(SaltMixin, generic.TemplateView): template_name = 'password_reset/reset_sent_failed.html' @@ -67,7 +68,6 @@ def get_context_data(self, **kwargs): recover_failed = RecoverFailed.as_view() - class Recover(SaltMixin, generic.FormView): case_sensitive = True form_class = PasswordRecoveryForm @@ -85,7 +85,6 @@ def get_success_url(self): else: return reverse(self.failure_url_name, args=[self.mail_signature]) - def get_context_data(self, **kwargs): kwargs['url'] = self.request.get_full_path() return super(Recover, self).get_context_data(**kwargs) @@ -114,7 +113,7 @@ def send_notification(self): subject = loader.render_to_string(self.email_subject_template_name, context).strip() result = send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, - [self.user.email], fail_silently=True) + [self.user.email], fail_silently=True) return result def form_valid(self, form): From 395f5a14e5a4f468a91a925df94be203db6a4dfc Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 18:19:00 +0800 Subject: [PATCH 15/34] restored custom user --- password_reset/forms.py | 7 +++++-- password_reset/tests/tests.py | 16 ++++++++-------- password_reset/utils.py | 6 ++++-- password_reset/views.py | 6 +++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index 5ff771d1..4d7e5d7f 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -6,7 +6,8 @@ try: - from captcha.fields import CaptchaField # uses django-simple-captcha==0.5.1 + if 'captcha' in settings.INSTALLED_APPS: + from captcha.fields import CaptchaField # uses django-simple-captcha except ImportError: CaptchaField = None pass @@ -94,9 +95,11 @@ def get_user_by_email(self, email): def get_user_by_both(self, username): key = '__%sexact' key = key % '' if self.case_sensitive else key % 'i' - #f = lambda field: Q(**{field + key: username}) + # f = lambda field: Q(**{field + key: username}) + def f(field): # to satisfy lint in Travis auto build on Github return Q(**{field + key: username}) + filters = f('username') | f('email') User = get_user_model() try: diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index f07550c4..2d28b22f 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -15,30 +15,31 @@ from ..utils import get_user_model """ +CustomUser = None +ExtensionUser = None Commented out this part due to incompatibilities with Django 1.8.4 See http://stackoverflow.com/questions/27433228/django-1-7-django-db-utils-operationalerror-no-such-table-auth-customuser +""" if django.VERSION >= (1, 5): from django.contrib.auth.tests.custom_user import ( # noqa CustomUser, ExtensionUser) else: CustomUser = None # noqa ExtensionUser = None # noqa -""" -CustomUser = None -ExtensionUser = None - if django.VERSION < (1, 6): COLON_SUFFIX = '' # Django 1.5 or lower do NOT auto add colon suffix else: COLON_SUFFIX = ':' # Django 1.6 or higher auto add colon suffix + # Test manually via -# ./manage.py test --settings password_reset.tests.settings password_reset.tests +# ./manage.py test --settings password_reset.tests.settings +# password_test.tests + class CustomUserVariants(type): def __new__(cls, name, bases, dct): - #if django.VERSION >= (1, 5) and settings.AUTH_USER_MODEL: - if 1>2: + if django.VERSION >= (1, 5): for custom_user in ['auth.CustomUser', 'auth.ExtensionUser']: suffix = custom_user.lower().replace('.', '_') for key, fn in list(dct.items()): @@ -48,7 +49,6 @@ def __new__(cls, name, bases, dct): AUTH_USER_MODEL=custom_user)(fn) return super(CustomUserVariants, cls).__new__(cls, name, bases, dct) - def create_user(): email = 'bar@example.com' password = 'pass' diff --git a/password_reset/utils.py b/password_reset/utils.py index 9721f5fe..bd45d3b9 100644 --- a/password_reset/utils.py +++ b/password_reset/utils.py @@ -2,9 +2,11 @@ from django.contrib.auth import get_user_model except ImportError: from django.contrib.auth.models import User - #get_user_model = lambda: User # noqa + # get_user_model = lambda: User # noqa + def my_user(): # to satisfy lint in Travis auto build on Github - return User # noqa + return User # noqa + get_user_model = my_user def get_username(user): diff --git a/password_reset/views.py b/password_reset/views.py index 18ec50eb..85cfefa9 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -113,15 +113,15 @@ def send_notification(self): subject = loader.render_to_string(self.email_subject_template_name, context).strip() result = send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, - [self.user.email], fail_silently=True) + [ self.user.email ], fail_silently=True) return result def form_valid(self, form): self.user = form.cleaned_data['user'] self.sent_success = self.send_notification() if ( - len(self.search_fields) == 1 and - self.search_fields[0] == 'username' + len(self.search_fields) == 1 and + self.search_fields[0] == 'username' ): # if we only search by username, don't disclose the user email # since it may now be public information. From bae7e50cf379a036ca5b13992a9fdf442d3697c3 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 18:27:16 +0800 Subject: [PATCH 16/34] fixed lint errors --- password_reset/tests/tests.py | 7 +------ password_reset/utils.py | 1 + password_reset/views.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index 2d28b22f..fef47201 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -14,12 +14,6 @@ from ..forms import PasswordRecoveryForm, PasswordResetForm, error_messages from ..utils import get_user_model -""" -CustomUser = None -ExtensionUser = None -Commented out this part due to incompatibilities with Django 1.8.4 -See http://stackoverflow.com/questions/27433228/django-1-7-django-db-utils-operationalerror-no-such-table-auth-customuser -""" if django.VERSION >= (1, 5): from django.contrib.auth.tests.custom_user import ( # noqa CustomUser, ExtensionUser) @@ -49,6 +43,7 @@ def __new__(cls, name, bases, dct): AUTH_USER_MODEL=custom_user)(fn) return super(CustomUserVariants, cls).__new__(cls, name, bases, dct) + def create_user(): email = 'bar@example.com' password = 'pass' diff --git a/password_reset/utils.py b/password_reset/utils.py index bd45d3b9..dc82fa82 100644 --- a/password_reset/utils.py +++ b/password_reset/utils.py @@ -9,6 +9,7 @@ def my_user(): # to satisfy lint in Travis auto build on Github get_user_model = my_user + def get_username(user): username_field = getattr(user, 'USERNAME_FIELD', 'username') return getattr(user, username_field) diff --git a/password_reset/views.py b/password_reset/views.py index 85cfefa9..e620822a 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -113,7 +113,7 @@ def send_notification(self): subject = loader.render_to_string(self.email_subject_template_name, context).strip() result = send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, - [ self.user.email ], fail_silently=True) + [self.user.email], fail_silently=True) return result def form_valid(self, form): From 95e735771534673f00eea5963d62afdf0b1a84a0 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Mon, 28 Mar 2016 23:11:51 +0800 Subject: [PATCH 17/34] dummy changes to invoke lint, which was not run due to travis server issues --- password_reset/tests/tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index fef47201..31f579a4 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -26,8 +26,7 @@ else: COLON_SUFFIX = ':' # Django 1.6 or higher auto add colon suffix -# Test manually via -# ./manage.py test --settings password_reset.tests.settings +# Test manually via ./manage.py test --settings password_reset.tests.settings # password_test.tests From fe8370210f4d52684aca39171b6700dba3c5bf58 Mon Sep 17 00:00:00 2001 From: Kuiyu CHANG Date: Tue, 29 Mar 2016 10:44:32 +0800 Subject: [PATCH 18/34] locale: removed old zh/, create link zh to zh-hans. --- password_reset/locale/zh | 1 + .../locale/zh/LC_MESSAGES/django.mo | Bin 2524 -> 0 bytes .../locale/zh/LC_MESSAGES/django.po | 122 ------------------ 3 files changed, 1 insertion(+), 122 deletions(-) create mode 120000 password_reset/locale/zh delete mode 100644 password_reset/locale/zh/LC_MESSAGES/django.mo delete mode 100644 password_reset/locale/zh/LC_MESSAGES/django.po diff --git a/password_reset/locale/zh b/password_reset/locale/zh new file mode 120000 index 00000000..80d1c227 --- /dev/null +++ b/password_reset/locale/zh @@ -0,0 +1 @@ +zh-hans \ No newline at end of file diff --git a/password_reset/locale/zh/LC_MESSAGES/django.mo b/password_reset/locale/zh/LC_MESSAGES/django.mo deleted file mode 100644 index f1f11c350efc46072a3314f8a06d3f5ebd8cce99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2524 zcmaKs{cjXS7{^CJg^Hs1mKdXxCd4+}9#_N!j?!?n2Q>EO(iaSxsN1`_yOq72V`qwe?DO)S zXXno?>n8}V*Kogq`w!f2;Xd~S9=J9;8Wl!uo)ZyKLURNH-Wx&guDvw z244Up;ESLRO24ACei3{L<6EG#TLh1Szk@O|(*@Lg~n zk~$3T2KOQNaqtU_e|uVeA9;olyb=b6Koi^v=D>~MU!e5+2;2<*1Il_gBj}r8Ehxt! zP}bQ4N;@6=1Uv@54*m{G`$ym_;3hbi{%i4&V;>j<_k(rd=b+6023V4uK+2*f&w}!j zI-$(s1-He$Dh-P{J%<54FV+Y7igD!n^SEWMQa@Rnyiitgf8@K|#~R$S4id5`OX-G5 z`*lGDvmEMh+9({GTZyKvb;Y%M(YU=dL$}uH30^NKo_^XZ79& zE5VR^M^z=O2DC29t+-*Q>Q_%AFo~2(r#6f7Bkw{tQWOSHanJKVr4J6#TvKkP5xD}I}BGZu_9KBV5y_3rFj*bxWAK!|fnF z(A(BVJ3{SY+8AyRwYKdGwX}4FyStlUY9%r#K48b<(Uo*xa!T%L6C# zb7x?gpTF<(uKnbV=G4F&8!t@FEf0-)m+vm+77GhGYciWOpt~O4Y%&t9a-(e zg~G(NcY8`jMM^8#Ro?0f*KI*E=`l<|dT~Qg&2%^M)@k z4V{qH5WunZ3 jvUAG$D#et>-tf44_FG@!{9Oc8!WFK}y9*Nt, YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-01-23 10:01+0800\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" - -#: forms.py:26 -msgid "Username" -msgstr "用户名" - -#: forms.py:27 -msgid "Email" -msgstr "邮箱" - -#: forms.py:28 -msgid "Username or Email" -msgstr "用户名或者邮箱" - -#: forms.py:48 forms.py:58 forms.py:70 -msgid "Sorry, this user doesn't exist." -msgstr "抱歉,用户名不存在." - -#: forms.py:72 -msgid "Unable to find user." -msgstr "找不到指定用户" - -#: forms.py:78 -msgid "New password" -msgstr "新密码" - -#: forms.py:82 -msgid "New password (confirm)" -msgstr "新密码(确认)" - -#: forms.py:94 -msgid "The two passwords didn't match." -msgstr "两次输入的密码不一致" - -#: templates/password_reset/recovery_done.html:3 -msgid "New password set" -msgstr "设置新密码" - -#: templates/password_reset/recovery_done.html:6 -msgid "" -"Your password has successfully been reset. You can use it right now on the " -"login page." -msgstr "您的密码已经重设成功,现在可以登录了." - -#: templates/password_reset/recovery_email.txt:1 -#, python-format -msgid "Dear %(username)s," -msgstr "您好 %(username)s," - -#: templates/password_reset/recovery_email.txt:3 -#, python-format -msgid "" -"You -- or someone pretending to be you -- has requested a password reset on " -"%(domain)s." -msgstr "您或者其它的人在 %(domain)s 申请了密码重置." - -#: templates/password_reset/recovery_email.txt:5 -msgid "You can set your new password by following this link:" -msgstr "您可以通过以下链接重置密码:" - -#: templates/password_reset/recovery_email.txt:9 -msgid "" -"If you don't want to reset your password, simply ignore this email and it " -"will stay unchanged." -msgstr "如果您不想重设密码,可以忽略此邮箱." - -#: templates/password_reset/recovery_email_subject.txt:1 -#, python-format -msgid "Password recovery on %(domain)s" -msgstr "在 %(domain)s 上重设密码" - -#: templates/password_reset/recovery_form.html:5 -msgid "Password recovery" -msgstr "重新设置密码" - -#: templates/password_reset/recovery_form.html:11 -msgid "Recover my password" -msgstr "重设密码" - -#: templates/password_reset/reset.html:5 -#, python-format -msgid "" -"Sorry, this password reset link is invalid. You can still request a new one." -msgstr "抱歉,链接已经失效,您可以在 请求一个新链接." - -#: templates/password_reset/reset.html:7 -#, python-format -msgid "Hi, %(username)s. Please choose your new password." -msgstr "您好,%(username)s. 请输入您的新密码." - -#: templates/password_reset/reset.html:11 -msgid "Set new password" -msgstr "设置新密码" - -#: templates/password_reset/reset_sent.html:4 -msgid "Password recovery sent" -msgstr "发送重设密码" - -#: templates/password_reset/reset_sent.html:7 -#, python-format -msgid "" -"An email was sent to %(email)s %(ago)s ago. Use the link in " -"it to set a new password." -msgstr "已经在%(ago)s以前向您的邮箱 %(email)s发送了邮件,根据邮件提示重新设置您的新密码." From ce5f13ce5fd494c4b110ac0a218439ad5152caaf Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 13:05:59 -0300 Subject: [PATCH 19/34] Adding method used to customize how to get the user into subclasses. --- password_reset/forms.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index 8dc0ad1d..2192adbd 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -56,11 +56,17 @@ def clean_username_or_email(self): return username + def get_user(self, *args, **kwargs): + """ Method used to customize how to get the user into subclasses. + :rtype: User object + """ + return get_user_model()._default_manager.get(*args, **kwargs) + def get_user_by_username(self, username): key = 'username__%sexact' % ('' if self.case_sensitive else 'i') User = get_user_model() try: - user = User._default_manager.get(**{key: username}) + user = self.get_user(**{key: username}) except User.DoesNotExist: raise forms.ValidationError(self.error_messages['not_found'], code='not_found') @@ -71,7 +77,7 @@ def get_user_by_email(self, email): key = 'email__%sexact' % ('' if self.case_sensitive else 'i') User = get_user_model() try: - user = User._default_manager.get(**{key: email}) + user = self.get_user(**{key: email}) except User.DoesNotExist: raise forms.ValidationError(self.error_messages['not_found'], code='not_found') @@ -86,7 +92,7 @@ def f(field): filters = f('username') | f('email') User = get_user_model() try: - user = User._default_manager.get(filters) + user = self.get_user(filters) except User.DoesNotExist: raise forms.ValidationError(self.error_messages['not_found'], code='not_found') From 9277411b80664b5a9e9d9c046caec941fd12a540 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 13:12:13 -0300 Subject: [PATCH 20/34] Ignore ide files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8c5e7b16..63aeb943 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ docs/_build *.pyc .tox build +.idea \ No newline at end of file From 65d8b6378bf1ad5514ec0cd9d4254d40f470d2a3 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 16:43:10 -0300 Subject: [PATCH 21/34] bug fix merge. --- .../templates/password_reset/recovery_email.html | 1 - password_reset/tests/tests.py | 12 ++---------- password_reset/views.py | 4 ++-- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/password_reset/templates/password_reset/recovery_email.html b/password_reset/templates/password_reset/recovery_email.html index 1313e2c5..1794071a 100644 --- a/password_reset/templates/password_reset/recovery_email.html +++ b/password_reset/templates/password_reset/recovery_email.html @@ -1,6 +1,5 @@ {% extends "password_reset/base_email.html" %} {% load i18n %} -{% load url from future %} {% block content %}

{% blocktrans %}Dear {{ username }},{% endblocktrans %}

diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index b5517cf3..45b7d1a3 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -1,6 +1,4 @@ -from unittest import SkipTest - -from django.contrib.auth import get_user_model +import django from django.core import mail from django.core.urlresolvers import reverse from django.test import TestCase @@ -14,13 +12,7 @@ from ..forms import PasswordRecoveryForm, PasswordResetForm, error_messages from ..utils import get_user_model - -if django.VERSION >= (1, 5): - from django.contrib.auth.tests.custom_user import ( # noqa - CustomUser, ExtensionUser) -else: - CustomUser = None # noqa - ExtensionUser = None # noqa +from .models import CustomUser, ExtensionUser if django.VERSION < (1, 6): COLON_SUFFIX = '' # Django 1.5 or lower do NOT auto add colon suffix diff --git a/password_reset/views.py b/password_reset/views.py index c746de76..ef6e38ea 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -128,8 +128,8 @@ def send_notification(self): subject, text_content, settings.DEFAULT_FROM_EMAIL, [self.user.email, ]) msg.attach_alternative(html_content, 'text/html') - - msg.send() + result = msg.send(fail_silently=True) + return result def form_valid(self, form): self.user = form.cleaned_data['user'] From be56e7195668cef49dfa836ab0d8e740273e1652 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 16:55:08 -0300 Subject: [PATCH 22/34] Moving compatibility code. --- password_reset/compat.py | 6 ++++++ password_reset/views.py | 7 +------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 password_reset/compat.py diff --git a/password_reset/compat.py b/password_reset/compat.py new file mode 100644 index 00000000..520376e9 --- /dev/null +++ b/password_reset/compat.py @@ -0,0 +1,6 @@ +def get_current_site(): + try: + from django.contrib.sites.shortcuts import get_current_site + except ImportError: + from django.contrib.sites.models import get_current_site + return get_current_site diff --git a/password_reset/views.py b/password_reset/views.py index ef6e38ea..c617e621 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -13,12 +13,7 @@ from django.utils.decorators import method_decorator from django.views import generic from django.views.decorators.debug import sensitive_post_parameters - -try: - from django.contrib.sites.shortcuts import get_current_site -except ImportError: - from django.contrib.sites.models import get_current_site - +from .compat import get_current_site from .forms import PasswordRecoveryForm, PasswordResetForm from .signals import user_recovers_password from .utils import get_user_model, get_username From e4952c32a469dba57e0db19bc7ec787ffad6a0ac Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 17:13:59 -0300 Subject: [PATCH 23/34] Moving compatibility code. --- password_reset/compat.py | 34 ++++++++++++++++++++++++++++++---- password_reset/forms.py | 4 +--- password_reset/tests/tests.py | 2 +- password_reset/utils.py | 15 --------------- password_reset/views.py | 5 +---- 5 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 password_reset/utils.py diff --git a/password_reset/compat.py b/password_reset/compat.py index 520376e9..3657414b 100644 --- a/password_reset/compat.py +++ b/password_reset/compat.py @@ -1,6 +1,32 @@ -def get_current_site(): +def get_user_model(): + """ + Returns the User model that is active in this project. + """ try: - from django.contrib.sites.shortcuts import get_current_site + from django.contrib.auth import get_user_model + User = get_user_model() except ImportError: - from django.contrib.sites.models import get_current_site - return get_current_site + from django.contrib.auth.models import User + return User + + +def get_username(user): + """ + Returns the name of the username field + :param user: User object instance + :return: str + """ + username_field = getattr(user, 'USERNAME_FIELD', 'username') + return getattr(user, username_field) + + +def get_current_site(*args, **kwargs): + """ + Checks if contrib.sites is installed and returns either the current + ``Site`` object or a ``RequestSite`` object based on the request. + """ + try: + from django.contrib.sites.shortcuts import get_current_site as current_site + except ImportError: + from django.contrib.sites.models import get_current_site as current_site + return current_site(*args, **kwargs) diff --git a/password_reset/forms.py b/password_reset/forms.py index 1c186f89..a04c47ca 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -3,7 +3,7 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.conf import settings - +from .compat import get_user_model try: if 'captcha' in settings.INSTALLED_APPS: @@ -12,8 +12,6 @@ CaptchaField = None -from .utils import get_user_model - error_messages = { 'not_found': _("Sorry, this user doesn't exist."), 'password_mismatch': _("The two passwords didn't match."), diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index 45b7d1a3..a9d76453 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -11,8 +11,8 @@ from unittest import SkipTest from ..forms import PasswordRecoveryForm, PasswordResetForm, error_messages -from ..utils import get_user_model from .models import CustomUser, ExtensionUser +from ..compat import get_user_model if django.VERSION < (1, 6): COLON_SUFFIX = '' # Django 1.5 or lower do NOT auto add colon suffix diff --git a/password_reset/utils.py b/password_reset/utils.py deleted file mode 100644 index dc82fa82..00000000 --- a/password_reset/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -try: - from django.contrib.auth import get_user_model -except ImportError: - from django.contrib.auth.models import User - # get_user_model = lambda: User # noqa - - def my_user(): # to satisfy lint in Travis auto build on Github - return User # noqa - - get_user_model = my_user - - -def get_username(user): - username_field = getattr(user, 'USERNAME_FIELD', 'username') - return getattr(user, username_field) diff --git a/password_reset/views.py b/password_reset/views.py index c617e621..1430834d 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -1,8 +1,6 @@ import datetime from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.sites.shortcuts import get_current_site from django.core import signing from django.core.mail import EmailMultiAlternatives from django.core.urlresolvers import reverse, reverse_lazy @@ -13,10 +11,9 @@ from django.utils.decorators import method_decorator from django.views import generic from django.views.decorators.debug import sensitive_post_parameters -from .compat import get_current_site +from .compat import get_current_site, get_user_model, get_username from .forms import PasswordRecoveryForm, PasswordResetForm from .signals import user_recovers_password -from .utils import get_user_model, get_username class SaltMixin(object): From d6338e5154cf2ab0749227ae00065209a65154ed Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 31 Aug 2017 22:39:15 -0300 Subject: [PATCH 24/34] Up version. --- password_reset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/__init__.py b/password_reset/__init__.py index 4ca39e7c..b4e35405 100644 --- a/password_reset/__init__.py +++ b/password_reset/__init__.py @@ -1 +1 @@ -__version__ = '0.8.2' +__version__ = '0.8.3' From c2a8d5b91185fdf8dd408b73224919a5d427438f Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 1 Sep 2017 09:12:07 -0300 Subject: [PATCH 25/34] Fix go to link. --- password_reset/templates/password_reset/base_email.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/templates/password_reset/base_email.html b/password_reset/templates/password_reset/base_email.html index 09b7ca8d..d7b8464a 100644 --- a/password_reset/templates/password_reset/base_email.html +++ b/password_reset/templates/password_reset/base_email.html @@ -172,7 +172,7 @@ - {% blocktrans with site_name=site.name %}Go to {{ site_name }}{% endblocktrans %} + {% blocktrans with site_name=site.name %}Go to {{ site_name }}{% endblocktrans %} From f71dae3c132b73b0777c00338cbfa3569ab66ff0 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 1 Sep 2017 09:59:08 -0300 Subject: [PATCH 26/34] Changing logic to work with captcha. --- password_reset/forms.py | 47 +++++++++++++++++++++++++++-------------- setup.py | 3 ++- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index a04c47ca..20f7e571 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -1,30 +1,45 @@ from django import forms +from django.core.exceptions import ImproperlyConfigured from django.core.validators import validate_email from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.conf import settings from .compat import get_user_model -try: - if 'captcha' in settings.INSTALLED_APPS: - from captcha.fields import CaptchaField # uses django-simple-captcha -except ImportError: - CaptchaField = None - - error_messages = { - 'not_found': _("Sorry, this user doesn't exist."), + 'not_found': _("Sorry, this user doesn't exist."), 'password_mismatch': _("The two passwords didn't match."), } +try: + # must be installed before. + if 'captcha' not in settings.INSTALLED_APPS: + raise ImproperlyConfigured("captcha is not installed.") + + # uses django-simple-captcha + from captcha.fields import CaptchaField + + + class CaptchaForm(forms.Form): + captcha = CaptchaField(label=_('Captcha')) # Optional Captcha + + def order_fields(self, field_order): + """https://docs.djangoproject.com/en/1.9/ref/forms/api/#django.forms.Form.order_fields""" + # Put the captcha at the bottom of the form + field_name = "captcha" + if field_order is None: + field_order = self.fields.keys() + field_order.pop(field_order.index(field_name)) + field_order.append(field_name) + return super(CaptchaForm, self).order_fields(field_order) + + +except (ImproperlyConfigured, ImportError): + CaptchaForm = forms.Form + -class PasswordRecoveryForm(forms.Form): +class PasswordRecoveryForm(CaptchaForm): username_or_email = forms.CharField() - try: - if CaptchaField and settings.CAPTCHA_CHALLENGE_FUNCT: # defined - captcha = CaptchaField(label=_('Captcha')) # Optional Captcha - except: - pass def __init__(self, *args, **kwargs): self.case_sensitive = kwargs.pop('case_sensitive', True) @@ -65,7 +80,7 @@ def clean_username_or_email(self): if recovery_only_active_users and not user_is_active: raise forms.ValidationError(_("Sorry, inactive users can't " - "recover their password.")) + "recover their password.")) return username @@ -100,7 +115,7 @@ def get_user_by_both(self, username): key = '__%sexact' key = key % '' if self.case_sensitive else key % 'i' - def f(field): # to satisfy lint in Travis auto build on Github + def f(field): # to satisfy lint in Travis auto build on Github return Q(**{field + key: username}) filters = f('username') | f('email') diff --git a/setup.py b/setup.py index b657ef5f..c1194090 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,8 @@ long_description=open('README.rst').read(), install_requires=[ 'Django>=1.4', - #'django-simple-captcha>=0.5.1', # will use captcha if installed + # It will be installed, but will only be used if properly configured. + 'django-simple-captcha>=0.5.1', ], classifiers=[ 'Development Status :: 4 - Beta', From 3546817408316d80b4e679419eaf1b430294cad1 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 1 Sep 2017 10:10:11 -0300 Subject: [PATCH 27/34] Up version. --- password_reset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/__init__.py b/password_reset/__init__.py index b4e35405..21320a81 100644 --- a/password_reset/__init__.py +++ b/password_reset/__init__.py @@ -1 +1 @@ -__version__ = '0.8.3' +__version__ = '0.8.4' From 519990fcb4364d5c71dad15c8fd2e554666006b7 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 16 Sep 2020 15:15:13 -0300 Subject: [PATCH 28/34] Add package compat. --- password_reset/compat/__init__.py | 34 +++++++++++++++++++++++++++++++ password_reset/compat/urls.py | 6 ++++++ 2 files changed, 40 insertions(+) create mode 100644 password_reset/compat/__init__.py create mode 100644 password_reset/compat/urls.py diff --git a/password_reset/compat/__init__.py b/password_reset/compat/__init__.py new file mode 100644 index 00000000..d2f743e8 --- /dev/null +++ b/password_reset/compat/__init__.py @@ -0,0 +1,34 @@ +# coding=utf-8 + +def get_user_model(): + """ + Returns the User model that is active in this project. + """ + try: + from django.contrib.auth import get_user_model + User = get_user_model() + except ImportError: + from django.contrib.auth.models import User + return User + + +def get_username(user): + """ + Returns the name of the username field + :param user: User object instance + :return: str + """ + username_field = getattr(user, 'USERNAME_FIELD', 'username') + return getattr(user, username_field) + + +def get_current_site(*args, **kwargs): + """ + Checks if contrib.sites is installed and returns either the current + ``Site`` object or a ``RequestSite`` object based on the request. + """ + try: + from django.contrib.sites.shortcuts import get_current_site as current_site + except ImportError: + from django.contrib.sites.models import get_current_site as current_site + return current_site(*args, **kwargs) diff --git a/password_reset/compat/urls.py b/password_reset/compat/urls.py new file mode 100644 index 00000000..54c1ce6c --- /dev/null +++ b/password_reset/compat/urls.py @@ -0,0 +1,6 @@ +# coding=utf-8 + +try: + from django.urls import reverse, reverse_lazy, NoReverseMatch +except ImportError: + from django.core.urlresolvers import reverse, reverse_lazy, NoReverseMatch \ No newline at end of file From 694802f7484909b90385b8158443985978e8bb31 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 16 Sep 2020 15:16:01 -0300 Subject: [PATCH 29/34] Fix import url reverse to django2. --- password_reset/compat.py | 32 -------------------------------- password_reset/tests/tests.py | 2 +- password_reset/views.py | 2 +- 3 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 password_reset/compat.py diff --git a/password_reset/compat.py b/password_reset/compat.py deleted file mode 100644 index 3657414b..00000000 --- a/password_reset/compat.py +++ /dev/null @@ -1,32 +0,0 @@ -def get_user_model(): - """ - Returns the User model that is active in this project. - """ - try: - from django.contrib.auth import get_user_model - User = get_user_model() - except ImportError: - from django.contrib.auth.models import User - return User - - -def get_username(user): - """ - Returns the name of the username field - :param user: User object instance - :return: str - """ - username_field = getattr(user, 'USERNAME_FIELD', 'username') - return getattr(user, username_field) - - -def get_current_site(*args, **kwargs): - """ - Checks if contrib.sites is installed and returns either the current - ``Site`` object or a ``RequestSite`` object based on the request. - """ - try: - from django.contrib.sites.shortcuts import get_current_site as current_site - except ImportError: - from django.contrib.sites.models import get_current_site as current_site - return current_site(*args, **kwargs) diff --git a/password_reset/tests/tests.py b/password_reset/tests/tests.py index a9d76453..7af52569 100644 --- a/password_reset/tests/tests.py +++ b/password_reset/tests/tests.py @@ -1,6 +1,6 @@ import django from django.core import mail -from django.core.urlresolvers import reverse +from password_reset.compat.urls import reverse from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone diff --git a/password_reset/views.py b/password_reset/views.py index 3ce7575b..16726eda 100644 --- a/password_reset/views.py +++ b/password_reset/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.core import signing from django.core.mail import EmailMultiAlternatives -from django.core.urlresolvers import reverse, reverse_lazy +from password_reset.compat.urls import reverse, reverse_lazy from django.shortcuts import get_object_or_404, redirect from django.http import Http404 from django.template import loader From 5c1018bb3c0513d0e08a18b93d35f8cf1153eb30 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 22 Sep 2020 17:34:55 -0300 Subject: [PATCH 30/34] v0.9.0 --- password_reset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/__init__.py b/password_reset/__init__.py index 21320a81..e4e49b3b 100644 --- a/password_reset/__init__.py +++ b/password_reset/__init__.py @@ -1 +1 @@ -__version__ = '0.8.4' +__version__ = '0.9.0' From 174f9a3da3a6d5b07a4b9ed01415b677b3507897 Mon Sep 17 00:00:00 2001 From: alexsilva Date: Thu, 25 Apr 2024 17:18:57 -0300 Subject: [PATCH 31/34] Replace 'ugettext_lazy' with 'gettext_lazy'. --- password_reset/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/forms.py b/password_reset/forms.py index 20f7e571..02367f04 100644 --- a/password_reset/forms.py +++ b/password_reset/forms.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.validators import validate_email from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.conf import settings from .compat import get_user_model From 0d0769b51c9a24b8df58acf0f2957ab144ab15fe Mon Sep 17 00:00:00 2001 From: alexsilva Date: Thu, 25 Apr 2024 17:27:08 -0300 Subject: [PATCH 32/34] =?UTF-8?q?#3122:=20remo=C3=A7=C3=A3o=20de=20'provid?= =?UTF-8?q?ing=5Fargs'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- password_reset/signals.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/password_reset/signals.py b/password_reset/signals.py index 6bf3823f..f3bd8381 100644 --- a/password_reset/signals.py +++ b/password_reset/signals.py @@ -1,6 +1,4 @@ from django.dispatch import Signal # signal sent when users successfully recover their passwords -user_recovers_password = Signal( - providing_args=['user', 'request'] -) +user_recovers_password = Signal() From ee22edbd6d7c9f22a8e395c93771900bcca1a56a Mon Sep 17 00:00:00 2001 From: alexsilva Date: Fri, 20 Sep 2024 17:30:56 -0300 Subject: [PATCH 33/34] update coding. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c1194090..46693b18 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding=utf-8 try: from setuptools import setup except ImportError: From fdc5f2bfa4d47e80137798a454e0cb894f276bc7 Mon Sep 17 00:00:00 2001 From: alexsilva Date: Fri, 20 Sep 2024 17:31:45 -0300 Subject: [PATCH 34/34] v1.0.0 --- password_reset/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/password_reset/__init__.py b/password_reset/__init__.py index e4e49b3b..1f356cc5 100644 --- a/password_reset/__init__.py +++ b/password_reset/__init__.py @@ -1 +1 @@ -__version__ = '0.9.0' +__version__ = '1.0.0'