├── .editorconfig
├── .gitignore
├── .pre-commit-config.yaml
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── email_registration
├── __init__.py
├── locale
│ └── de
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── models.py
├── signals.py
├── templates
│ └── registration
│ │ ├── email_registration_email.txt
│ │ ├── email_registration_form.html
│ │ ├── email_registration_include.html
│ │ ├── email_registration_sent.html
│ │ └── password_set_form.html
├── urls.py
├── utils.py
└── views.py
├── setup.cfg
├── setup.py
└── tests
├── .gitignore
├── cov.sh
├── manage.py
├── requirements.txt
├── testapp
├── __init__.py
├── models.py
├── settings.py
├── templates
│ ├── 404.html
│ ├── base.html
│ ├── plain.html
│ └── registration
│ │ └── login.html
├── test_registration.py
└── urls.py
├── tox-update-venvs.sh
└── tox.ini
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | end_of_line = lf
6 | insert_final_newline = true
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [*.py]
13 | indent_size = 4
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *~
3 | .*.swp
4 | \#*#
5 | /secrets.py
6 | .DS_Store
7 | ._*
8 | /MANIFEST
9 | /_build
10 | /build
11 | /dist
12 | /django_email_registration.egg-info
13 | /tests/venv
14 | .tox
15 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: ".yarn/|yarn.lock"
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v4.1.0
5 | hooks:
6 | - id: check-added-large-files
7 | - id: check-merge-conflict
8 | - id: end-of-file-fixer
9 | - id: trailing-whitespace
10 | - repo: https://github.com/asottile/pyupgrade
11 | rev: v2.31.0
12 | hooks:
13 | - id: pyupgrade
14 | args: [--py38-plus]
15 | - repo: https://github.com/adamchainz/django-upgrade
16 | rev: 1.4.0
17 | hooks:
18 | - id: django-upgrade
19 | args: [--target-version, "3.2"]
20 | - repo: https://github.com/pycqa/isort
21 | rev: 5.10.1
22 | hooks:
23 | - id: isort
24 | args: [--profile=black, --lines-after-imports=2, --combine-as]
25 | - repo: https://github.com/psf/black
26 | rev: 22.1.0
27 | hooks:
28 | - id: black
29 | - repo: https://github.com/pycqa/flake8
30 | rev: 4.0.1
31 | hooks:
32 | - id: flake8
33 | args: ["--ignore=E203,E501,W503"]
34 | - repo: https://github.com/pre-commit/mirrors-prettier
35 | rev: v2.5.1
36 | hooks:
37 | - id: prettier
38 | args: [--list-different, --no-semi]
39 | exclude: "^conf/|.*\\.html$"
40 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | cache: pip
3 | sudo: false
4 | python:
5 | - "3.5"
6 | - "2.7"
7 | env:
8 | - REQ="Django>=1.11,<2.0"
9 | - REQ="Django>=2.0,<2.1"
10 | matrix:
11 | exclude:
12 | - python: "2.7"
13 | env: REQ="Django>=2.0,<2.1"
14 | install:
15 | - pip install -U pip wheel
16 | - pip install $REQ flake8 towel
17 | - python setup.py install
18 | # command to run tests, e.g. python setup.py test
19 | script: "cd tests && ./manage.py test testapp && cd .. && flake8 ."
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012, Matthias Kestenholz and individual contributors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of django-email-registration nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include MANIFEST.in
3 | include README.rst
4 | recursive-include email_registration/static *
5 | recursive-include email_registration/locale *
6 | recursive-include email_registration/templates *
7 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =========================
2 | django-email-registration
3 | =========================
4 |
5 | The eleventy-eleventh email registration app for Django.
6 |
7 | But this one does not feed your cat.
8 |
9 | .. image:: https://travis-ci.org/matthiask/django-email-registration.png?branch=master
10 | :target: https://travis-ci.org/matthiask/django-email-registration
11 |
12 |
13 | Usage
14 | =====
15 |
16 | This example assumes you are using a recent version of Django, jQuery and
17 | Twitter Bootstrap.
18 |
19 | 1. Install ``django-email-registration`` using pip.
20 |
21 | 2. Copy this code somewhere on your login or registration page::
22 |
23 |
{% trans "Send an activation link" %}
24 |
34 |
35 |
48 |
49 | (Alternatively, include the template snippet
50 | ``registration/email_registration_include.html`` somewhere.)
51 |
52 | 3. Add ``email_registration`` to ``INSTALLED_APPS`` and include
53 | ``email_registration.urls`` somewhere in your URLconf.
54 |
55 | 4. Make sure that Django is able to
56 | `send emails `_.
57 |
58 | 5. Presto.
59 |
--------------------------------------------------------------------------------
/email_registration/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = (0, 5, 0)
2 | __version__ = ".".join(map(str, VERSION))
3 |
--------------------------------------------------------------------------------
/email_registration/locale/de/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthiask/django-email-registration/a014c5af6a470f02693a7beb38559887e1fa8eb8/email_registration/locale/de/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/email_registration/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2013-06-10 12:25+0200\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n"
20 |
21 | #: utils.py:79
22 | msgid "The link is expired. Please request another registration link."
23 | msgstr "Der Link ist verfallen. Bitte beantragen Sie einen neuen Link."
24 |
25 | #: utils.py:84
26 | msgid "Unable to verify the signature. Please request a new registration link."
27 | msgstr ""
28 | "Konnte die Signatur nicht verifizieren. Bitte beantragen Sie einen neuen "
29 | "Link."
30 |
31 | #: utils.py:90 utils.py:99
32 | msgid ""
33 | "Something went wrong while decoding the registration request. Please try "
34 | "again."
35 | msgstr "Etwas ist schiefgelaufen. Bitte versuchen Sie es nochmals."
36 |
37 | #: utils.py:103
38 | msgid "The link has already been used."
39 | msgstr "Der Link wurde schon verwendet."
40 |
41 | #: views.py:19 views.py:21
42 | msgid "email address"
43 | msgstr "Emailadresse"
44 |
45 | #: views.py:28 views.py:61
46 | msgid ""
47 | "This email address already exists as an account. Did you want to reset your "
48 | "password?"
49 | msgstr ""
50 | "Die Emailadresse existiert schon im System. Wollten Sie Ihr Passwort "
51 | "zurücksetzen?"
52 |
53 | #: views.py:82
54 | msgid "Successfully set the new password. Please login now."
55 | msgstr ""
56 | "Das neue Passwort wurde erfolgreich gesetzt. Sie können sich jetzt anmelden."
57 |
58 | #: views.py:87
59 | msgid "Please set a password."
60 | msgstr "Bitte setzen Sie ein neues Passwort."
61 |
62 | #: templates/registration/email_registration_email.txt:2
63 | msgid "Registration link"
64 | msgstr "Registrierungslink"
65 |
66 | #: templates/registration/email_registration_email.txt:4
67 | msgid "Click the following link to activate your account:"
68 | msgstr "Besuchen Sie den folgenden Link um Ihr Konto zu aktivieren:"
69 |
70 | #: templates/registration/email_registration_email.txt:8
71 | msgid ""
72 | "Please note that this link expires after 3 days. If you wait\n"
73 | "longer, you'll have to request another registration link."
74 | msgstr ""
75 | "Dieser Link ist nur 3 Tage gültig. Wenn Sie länger warten,\n"
76 | "müssen Sie einen neuen Link anfordern."
77 |
78 | #: templates/registration/email_registration_email.txt:11
79 | msgid "Best regards"
80 | msgstr "Freundliche Grüsse"
81 |
82 | #: templates/registration/email_registration_form.html:11
83 | #: templates/registration/email_registration_include.html:11
84 | msgid "Register"
85 | msgstr "Registrieren"
86 |
87 | #: templates/registration/email_registration_include.html:7
88 | msgid "Email address"
89 | msgstr "Emailadresse"
90 |
91 | #: templates/registration/email_registration_sent.html:3
92 | #, python-format
93 | msgid ""
94 | "We sent you an email to %(email)s.\n"
95 | " Please click the contained link to finish your registration."
96 | msgstr ""
97 | "Wir haben ein Email an %(email)s verschickt. Bitte besuchen Sie den darin "
98 | "enthaltenen Link um die Registrierung abzuschliessen."
99 |
100 | #: templates/registration/password_set_form.html:4
101 | #: templates/registration/password_set_form.html:8
102 | #: templates/registration/password_set_form.html:21
103 | msgid "Set password"
104 | msgstr "Passwort setzen"
105 |
106 | #~ msgid "Successfully created a new user. Please set a password."
107 | #~ msgstr ""
108 | #~ "Ein Nutzer wurde erfolgreich erstellt. Bitte setzen Sie ein Passwort."
109 |
--------------------------------------------------------------------------------
/email_registration/models.py:
--------------------------------------------------------------------------------
1 | # Intentionally left empty. We don't need no models!
2 |
--------------------------------------------------------------------------------
/email_registration/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import Signal
2 |
3 |
4 | password_set = Signal()
5 |
--------------------------------------------------------------------------------
/email_registration/templates/registration/email_registration_email.txt:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% trans "Registration link" %}
3 |
4 | {% trans "Click the following link to activate your account:" %}
5 |
6 | {{ url }}
7 |
8 | {% blocktrans %}Please note that this link expires after 3 days. If you wait
9 | longer, you'll have to request another registration link.{% endblocktrans %}
10 |
11 | {% trans "Best regards" %}
12 |
--------------------------------------------------------------------------------
/email_registration/templates/registration/email_registration_form.html:
--------------------------------------------------------------------------------
1 | {% load i18n towel_form_tags %}
2 |
16 |
--------------------------------------------------------------------------------
/email_registration/templates/registration/email_registration_include.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/email_registration/templates/registration/email_registration_sent.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
{% blocktrans %}We sent you an email to {{ email }}.
4 | Please click the contained link to finish your registration.{% endblocktrans %}
5 |
6 |
--------------------------------------------------------------------------------
/email_registration/templates/registration/password_set_form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load i18n towel_form_tags %}
3 |
4 | {% block title %}{% trans "Set password" %} - {{ block.super }}{% endblock %}
5 |
6 | {% block page-header %}
7 |
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/email_registration/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from email_registration.views import email_registration_confirm, email_registration_form
4 |
5 |
6 | urlpatterns = [
7 | path(
8 | "",
9 | email_registration_form,
10 | name="email_registration_form",
11 | ),
12 | path(
13 | "/",
14 | email_registration_confirm,
15 | name="email_registration_confirm",
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/email_registration/utils.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.core import signing
3 | from django.core.mail import EmailMultiAlternatives
4 | from django.template.loader import TemplateDoesNotExist, render_to_string
5 | from django.utils.http import int_to_base36
6 | from django.utils.translation import gettext as _
7 |
8 |
9 | try:
10 | from django.urls import reverse
11 | except ImportError: # pragma: no cover
12 | from django.core.urlresolvers import reverse
13 |
14 |
15 | def get_signer(salt="email_registration"):
16 | """
17 | Returns the signer instance used to sign and unsign the registration
18 | link tokens
19 | """
20 | return signing.TimestampSigner(salt=salt)
21 |
22 |
23 | def get_last_login_timestamp(user):
24 | """
25 | Django 1.7 allows the `last_login` timestamp to be `None` for new users.
26 | """
27 | return int(user.last_login.strftime("%s")) if user.last_login else 0
28 |
29 |
30 | def get_confirmation_url(email, request, user=None):
31 | """
32 | Returns the confirmation URL
33 | """
34 | code = [email, "", ""]
35 | if user:
36 | code[1] = str(user.id)
37 | code[2] = int_to_base36(get_last_login_timestamp(user))
38 |
39 | return request.build_absolute_uri(
40 | reverse(
41 | "email_registration_confirm",
42 | kwargs={
43 | "code": get_signer().sign(":".join(code)),
44 | },
45 | )
46 | )
47 |
48 |
49 | def send_registration_mail(email, request, user=None):
50 | """
51 | Sends the registration mail
52 |
53 | * ``email``: The email address where the registration link should be
54 | sent to.
55 | * ``request``: A HTTP request instance, used to construct the complete
56 | URL (including protocol and domain) for the registration link.
57 | * ``user``: Optional user instance. If the user exists already and you
58 | just want to send a link where the user can choose his/her password.
59 |
60 | The mail is rendered using the following two templates:
61 |
62 | * ``registration/email_registration_email.txt``: The first line of this
63 | template will be the subject, the third to the last line the body of the
64 | email.
65 | * ``registration/email_registration_email.html``: The body of the HTML
66 | version of the mail. This template is **NOT** available by default and
67 | is not required either.
68 | """
69 |
70 | render_to_mail(
71 | "registration/email_registration_email",
72 | {
73 | "url": get_confirmation_url(email, request, user=user),
74 | },
75 | to=[email],
76 | ).send()
77 |
78 |
79 | class InvalidCode(Exception):
80 | """Problems occurred during decoding the registration link"""
81 |
82 | pass
83 |
84 |
85 | def decode(code, max_age=3 * 86400):
86 | """
87 | Decodes the code from the registration link and returns a tuple consisting
88 | of the verified email address and the associated user instance or ``None``
89 | if no user was passed to ``send_registration_mail``
90 |
91 | This method raises ``InvalidCode`` exceptions containing an translated
92 | message what went wrong suitable for presenting directly to the user.
93 | """
94 | try:
95 | data = get_signer().unsign(code, max_age=max_age)
96 | except signing.SignatureExpired:
97 | raise InvalidCode(
98 | _("The link is expired. Please request another registration link.")
99 | )
100 |
101 | except signing.BadSignature:
102 | raise InvalidCode(
103 | _(
104 | "Unable to verify the signature. Please request a new"
105 | " registration link."
106 | )
107 | )
108 |
109 | parts = data.rsplit(":", 2)
110 | if len(parts) != 3:
111 | raise InvalidCode(
112 | _(
113 | "Something went wrong while decoding the"
114 | " registration request. Please try again."
115 | )
116 | )
117 |
118 | email, uid, timestamp = parts
119 | if uid and timestamp:
120 | try:
121 | user = User.objects.get(pk=uid)
122 | except (User.DoesNotExist, TypeError, ValueError):
123 | raise InvalidCode(
124 | _(
125 | "Something went wrong while decoding the"
126 | " registration request. Please try again."
127 | )
128 | )
129 |
130 | if timestamp != int_to_base36(get_last_login_timestamp(user)):
131 | raise InvalidCode(_("The link has already been used."))
132 |
133 | else:
134 | user = None
135 |
136 | return email, user
137 |
138 |
139 | def render_to_mail(template, context, **kwargs):
140 | """
141 | Renders a mail and returns the resulting ``EmailMultiAlternatives``
142 | instance
143 |
144 | * ``template``: The base name of the text and HTML (optional) version of
145 | the mail.
146 | * ``context``: The context used to render the mail. This context instance
147 | should contain everything required.
148 | * Additional keyword arguments are passed to the ``EmailMultiAlternatives``
149 | instantiation. Use those to specify the ``to``, ``headers`` etc.
150 | arguments.
151 |
152 | Usage example::
153 |
154 | # Render the template myproject/hello_mail.txt (first line contains
155 | # the subject, third to last the body) and optionally the template
156 | # myproject/hello_mail.html containing the alternative HTML
157 | # representation.
158 | message = render_to_mail('myproject/hello_mail', {}, to=[email])
159 | message.send()
160 | """
161 | lines = iter(render_to_string("%s.txt" % template, context).splitlines())
162 |
163 | subject = ""
164 | while True:
165 | line = next(lines)
166 | if line:
167 | subject = line
168 | break
169 |
170 | body = "\n".join(lines).strip("\n")
171 | message = EmailMultiAlternatives(subject=subject, body=body, **kwargs)
172 |
173 | try:
174 | message.attach_alternative(
175 | render_to_string("%s.html" % template, context), "text/html"
176 | )
177 | except TemplateDoesNotExist:
178 | pass
179 |
180 | return message
181 |
--------------------------------------------------------------------------------
/email_registration/views.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import messages
3 | from django.contrib.auth import get_user_model
4 | from django.contrib.auth.forms import SetPasswordForm
5 | from django.core.exceptions import FieldDoesNotExist
6 | from django.shortcuts import redirect, render
7 | from django.utils.crypto import get_random_string
8 | from django.utils.translation import gettext as _, gettext_lazy
9 | from django.views.decorators.http import require_POST
10 |
11 | from email_registration.signals import password_set
12 | from email_registration.utils import InvalidCode, decode, send_registration_mail
13 |
14 |
15 | User = get_user_model()
16 | USERNAME_FIELD = User.USERNAME_FIELD
17 |
18 |
19 | class RegistrationForm(forms.Form):
20 | email = forms.EmailField(
21 | label=gettext_lazy("email address"),
22 | max_length=75,
23 | widget=forms.TextInput(
24 | attrs={
25 | "placeholder": gettext_lazy("email address"),
26 | }
27 | ),
28 | )
29 |
30 | def clean_email(self):
31 | email = self.cleaned_data.get("email")
32 | if email and User.objects.filter(email=email).exists():
33 | raise forms.ValidationError(
34 | _(
35 | "This email address already exists as an account."
36 | " Did you want to reset your password?"
37 | )
38 | )
39 | return email
40 |
41 |
42 | @require_POST
43 | def email_registration_form(request, form_class=RegistrationForm):
44 | # TODO unajaxify this view for the release?
45 | form = form_class(request.POST)
46 |
47 | if form.is_valid():
48 | email = form.cleaned_data["email"]
49 | send_registration_mail(email, request)
50 |
51 | return render(
52 | request,
53 | "registration/email_registration_sent.html",
54 | {
55 | "email": email,
56 | },
57 | )
58 |
59 | return render(
60 | request,
61 | "registration/email_registration_form.html",
62 | {
63 | "form": form,
64 | },
65 | )
66 |
67 |
68 | def email_registration_confirm(
69 | request, code, max_age=3 * 86400, form_class=SetPasswordForm
70 | ):
71 | try:
72 | email, user = decode(code, max_age=max_age)
73 | except InvalidCode as exc:
74 | messages.error(request, "%s" % exc)
75 | return redirect("/")
76 |
77 | if not user:
78 | if User.objects.filter(email=email).exists():
79 | messages.error(
80 | request,
81 | _(
82 | "This email address already exists as an account."
83 | " Did you want to reset your password?"
84 | ),
85 | )
86 | return redirect("/")
87 |
88 | username_field = User._meta.get_field(USERNAME_FIELD)
89 |
90 | kwargs = {}
91 | if username_field.name == "email":
92 | kwargs["email"] = email
93 | else:
94 | username = email
95 |
96 | # If email exceeds max length of field set username to random
97 | # string
98 | max_length = username_field.max_length
99 | if len(username) > max_length:
100 | username = get_random_string(25 if max_length >= 25 else max_length)
101 | kwargs[username_field.name] = username
102 |
103 | # Set value for 'email' field in case the user model has one
104 | try:
105 | User._meta.get_field("email")
106 | kwargs["email"] = email
107 | except FieldDoesNotExist:
108 | pass
109 |
110 | user = User(**kwargs)
111 |
112 | if request.method == "POST":
113 | form = form_class(user, request.POST)
114 | if form.is_valid():
115 | user = form.save()
116 |
117 | password_set.send(
118 | sender=user.__class__,
119 | request=request,
120 | user=user,
121 | password=form.cleaned_data.get("new_password1"),
122 | )
123 |
124 | messages.success(
125 | request, _("Successfully set the new password. Please login now.")
126 | )
127 |
128 | return redirect("login")
129 |
130 | else:
131 | messages.success(request, _("Please set a password."))
132 | form = form_class(user)
133 |
134 | return render(
135 | request,
136 | "registration/password_set_form.html",
137 | {
138 | "form": form,
139 | },
140 | )
141 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude=venv,.tox,docs
3 |
4 | [bdist_wheel]
5 | universal = 1
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 |
5 | from setuptools import find_packages, setup
6 |
7 |
8 | def read(filename):
9 | return open(os.path.join(os.path.dirname(__file__), filename)).read()
10 |
11 |
12 | setup(
13 | name="django-email-registration",
14 | version=__import__("email_registration").__version__,
15 | description="So simple you'll burst into tears right away.",
16 | long_description=read("README.rst"),
17 | author="Matthias Kestenholz",
18 | author_email="mk@406.ch",
19 | url="http://github.com/matthiask/django-email-registration/",
20 | license="BSD License",
21 | platforms=["OS Independent"],
22 | packages=find_packages(),
23 | include_package_data=True,
24 | classifiers=[
25 | "Development Status :: 5 - Production/Stable",
26 | "Environment :: Web Environment",
27 | "Framework :: Django",
28 | "Intended Audience :: Developers",
29 | "License :: OSI Approved :: BSD License",
30 | "Operating System :: OS Independent",
31 | "Programming Language :: Python",
32 | "Programming Language :: Python :: 2",
33 | "Programming Language :: Python :: 2.7",
34 | "Programming Language :: Python :: 3",
35 | "Programming Language :: Python :: 3.4",
36 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
37 | "Topic :: Software Development",
38 | "Topic :: Software Development :: Libraries :: Application Frameworks",
39 | ],
40 | zip_safe=False,
41 | )
42 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | /.coverage
2 | /htmlcov
3 |
--------------------------------------------------------------------------------
/tests/cov.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | venv/bin/coverage run --branch --include="*email_registration/*" --omit="*tests*" ./manage.py test testapp
3 | venv/bin/coverage html
4 |
--------------------------------------------------------------------------------
/tests/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 | from os.path import abspath, dirname
5 |
6 |
7 | if __name__ == "__main__":
8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings")
9 |
10 | sys.path.insert(0, dirname(dirname(abspath(__file__))))
11 |
12 | from django.core.management import execute_from_command_line
13 |
14 | execute_from_command_line(sys.argv)
15 |
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | Django
2 | coverage
3 | towel
4 |
--------------------------------------------------------------------------------
/tests/testapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthiask/django-email-registration/a014c5af6a470f02693a7beb38559887e1fa8eb8/tests/testapp/__init__.py
--------------------------------------------------------------------------------
/tests/testapp/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matthiask/django-email-registration/a014c5af6a470f02693a7beb38559887e1fa8eb8/tests/testapp/models.py
--------------------------------------------------------------------------------
/tests/testapp/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | SITE_ID = 1
5 |
6 | DATABASES = {
7 | "default": {
8 | "ENGINE": "django.db.backends.sqlite3",
9 | "NAME": ":memory:",
10 | }
11 | }
12 |
13 | INSTALLED_APPS = [
14 | "django.contrib.auth",
15 | "django.contrib.admin",
16 | "django.contrib.contenttypes",
17 | "django.contrib.sessions",
18 | "django.contrib.staticfiles",
19 | "django.contrib.messages",
20 | "testapp",
21 | "towel",
22 | "email_registration",
23 | ]
24 |
25 | MEDIA_ROOT = "/media/"
26 | STATIC_URL = "/static/"
27 | BASEDIR = os.path.dirname(__file__)
28 | MEDIA_ROOT = os.path.join(BASEDIR, "media/")
29 | STATIC_ROOT = os.path.join(BASEDIR, "static/")
30 | SECRET_KEY = "supersikret"
31 | LOGIN_REDIRECT_URL = "/?login=1"
32 |
33 | ROOT_URLCONF = "testapp.urls"
34 | LANGUAGES = (("en", "English"), ("de", "German"))
35 | MIDDLEWARE = (
36 | "django.middleware.common.CommonMiddleware",
37 | "django.contrib.sessions.middleware.SessionMiddleware",
38 | "django.middleware.csrf.CsrfViewMiddleware",
39 | "django.contrib.auth.middleware.AuthenticationMiddleware",
40 | "django.contrib.messages.middleware.MessageMiddleware",
41 | )
42 | TEMPLATES = [
43 | {
44 | "BACKEND": "django.template.backends.django.DjangoTemplates",
45 | "DIRS": [
46 | os.path.join(BASEDIR, "templates"),
47 | ],
48 | "OPTIONS": {
49 | "context_processors": [
50 | "django.template.context_processors.debug",
51 | "django.template.context_processors.request",
52 | "django.contrib.auth.context_processors.auth",
53 | "django.contrib.messages.context_processors.messages",
54 | ],
55 | "loaders": [
56 | "django.template.loaders.filesystem.Loader",
57 | "django.template.loaders.app_directories.Loader",
58 | ],
59 | },
60 | },
61 | ]
62 |
--------------------------------------------------------------------------------
/tests/testapp/templates/404.html:
--------------------------------------------------------------------------------
1 | Page not found
2 |
--------------------------------------------------------------------------------
/tests/testapp/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}{% endblock %}
5 |
6 |
7 |
8 | {% if messages %}
9 |
10 | {% for message in messages %}
11 | - {{ message }}
12 | {% endfor %}
13 |
14 | {% endif %}
15 | {% block content %}{% endblock %}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/testapp/templates/plain.html:
--------------------------------------------------------------------------------
1 | {% block title %}{% endblock %}
2 | {% block content %}{% endblock %}
3 |
--------------------------------------------------------------------------------
/tests/testapp/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/tests/testapp/test_registration.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | from urllib.parse import unquote
4 |
5 | from django.contrib.auth.models import User
6 | from django.core import mail
7 | from django.test import TestCase
8 | from django.test.client import RequestFactory
9 | from django.utils import timezone
10 |
11 |
12 | try:
13 | from django.urls import reverse
14 | except ImportError: # pragma: no cover
15 | from django.core.urlresolvers import reverse
16 |
17 | from email_registration.utils import get_signer, send_registration_mail
18 |
19 |
20 | def _messages(response):
21 | return [m.message for m in response.context["messages"]]
22 |
23 |
24 | class RegistrationTest(TestCase):
25 | def test_registration(self):
26 | response = self.client.get("/er/")
27 |
28 | self.assertEqual(response.status_code, 405)
29 | self.assertEqual(response["Allow"], "POST")
30 |
31 | response = self.client.post(
32 | "/er/",
33 | {
34 | "email": "test@example.com",
35 | },
36 | )
37 | self.assertContains(response, "We sent you an email to test@example.com.")
38 |
39 | self.assertEqual(len(mail.outbox), 1)
40 | body = mail.outbox[0].body
41 | url = unquote([line for line in body.splitlines() if "testserver" in line][0])
42 |
43 | self.assertTrue("http://testserver/er/test@example.com:::" in url)
44 |
45 | response = self.client.get(url)
46 | self.assertContains(response, 'id="id_new_password2"')
47 |
48 | response = self.client.post(
49 | url,
50 | {
51 | "new_password1": "pass",
52 | "new_password2": "passss",
53 | },
54 | )
55 | self.assertEqual(response.status_code, 200)
56 |
57 | response = self.client.post(
58 | url,
59 | {
60 | "new_password1": "pass",
61 | "new_password2": "pass",
62 | },
63 | )
64 | self.assertRedirects(response, "/ac/login/")
65 |
66 | user = User.objects.get()
67 | self.assertEqual(user.username, "test@example.com")
68 | self.assertEqual(user.email, "test@example.com")
69 | self.assertEqual(user.is_active, True)
70 |
71 | response = self.client.post(
72 | "/ac/login/",
73 | {
74 | "username": "test@example.com",
75 | "password": "pass",
76 | },
77 | )
78 | self.assertRedirects(response, "/?login=1")
79 |
80 | def test_existing_user(self):
81 | user = User.objects.create(
82 | username="test",
83 | )
84 |
85 | request = RequestFactory().get("/")
86 | send_registration_mail("test@example.com", request, user=user)
87 |
88 | self.assertEqual(len(mail.outbox), 1)
89 | body = mail.outbox[0].body
90 | url = unquote([line for line in body.splitlines() if "testserver" in line][0])
91 |
92 | self.assertTrue(
93 | re.match(r"http://testserver/er/test@example.com:\d+:\w+:", url)
94 | )
95 |
96 | response = self.client.get(url)
97 |
98 | self.assertContains(response, 'id="id_new_password2"')
99 |
100 | response = self.client.post(
101 | url,
102 | {
103 | "new_password1": "pass",
104 | "new_password2": "pass",
105 | },
106 | )
107 | self.assertRedirects(response, "/ac/login/")
108 |
109 | user = User.objects.get()
110 | self.assertEqual(user.username, "test")
111 | self.assertEqual(user.email, "")
112 | self.assertTrue(user.check_password("pass"))
113 |
114 | time.sleep(1.1)
115 | user.last_login = timezone.now()
116 | user.save()
117 |
118 | response = self.client.get(url, follow=True)
119 | self.assertEqual(_messages(response), ["The link has already been used."])
120 | self.assertRedirects(response, "/")
121 |
122 | response = self.client.get(
123 | url.replace("/er/", "/er-quick/", 1),
124 | follow=True,
125 | )
126 | self.assertEqual(
127 | _messages(response),
128 | ["The link is expired. Please request another registration link."],
129 | )
130 |
131 | response = self.client.get(
132 | url.replace("com:", "ch:", 1),
133 | follow=True,
134 | )
135 | self.assertEqual(
136 | _messages(response),
137 | [
138 | "Unable to verify the signature."
139 | " Please request a new registration link."
140 | ],
141 | )
142 |
143 | user.delete()
144 | response = self.client.get(url, follow=True)
145 | self.assertEqual(
146 | _messages(response),
147 | [
148 | "Something went wrong while decoding the"
149 | " registration request. Please try again."
150 | ],
151 | )
152 |
153 | def test_already_existing_email(self):
154 | user = User.objects.create(
155 | username="test@example.com",
156 | email="test@example.com",
157 | )
158 | response = self.client.post(
159 | "/er/",
160 | {
161 | "email": user.email,
162 | },
163 | )
164 | self.assertContains(
165 | response, "Did you want to reset your password?", status_code=200
166 | )
167 |
168 | def test_user_created_in_the_meantime(self):
169 | response = self.client.post(
170 | "/er/",
171 | {
172 | "email": "test@example.com",
173 | },
174 | )
175 | self.assertContains(response, "We sent you an email to test@example.com.")
176 |
177 | self.assertEqual(len(mail.outbox), 1)
178 | body = mail.outbox[0].body
179 | url = unquote([line for line in body.splitlines() if "testserver" in line][0])
180 |
181 | self.assertTrue("http://testserver/er/test@example.com:::" in url)
182 |
183 | User.objects.create_user("test", "test@example.com", "blaa")
184 |
185 | response = self.client.get(url, follow=True)
186 | self.assertEqual(
187 | _messages(response),
188 | [
189 | "This email address already exists as an account."
190 | " Did you want to reset your password?"
191 | ],
192 | )
193 |
194 | def test_crap(self):
195 | user = User.objects.create_user("test", "test@example.com", "pass")
196 |
197 | code = [
198 | user.email,
199 | str(user.id),
200 | # Intentionally forget the timestamp.
201 | ]
202 |
203 | url = reverse(
204 | "email_registration_confirm",
205 | kwargs={
206 | "code": get_signer().sign(":".join(code)),
207 | },
208 | )
209 |
210 | response = self.client.get(url, follow=True)
211 | self.assertEqual(
212 | _messages(response),
213 | [
214 | "Something went wrong while decoding the"
215 | " registration request. Please try again."
216 | ],
217 | )
218 |
--------------------------------------------------------------------------------
/tests/testapp/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns
3 | from django.urls import include, path
4 | from django.views import generic
5 |
6 | from email_registration.views import email_registration_confirm
7 |
8 |
9 | urlpatterns = [
10 | path("admin/", admin.site.urls),
11 | path("", generic.TemplateView.as_view(template_name="base.html")),
12 | path("ac/", include("django.contrib.auth.urls")),
13 | path("er/", include("email_registration.urls")),
14 | path("er-quick//", email_registration_confirm, {"max_age": 1}),
15 | ] + staticfiles_urlpatterns()
16 |
--------------------------------------------------------------------------------
/tests/tox-update-venvs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | (
4 | cd .tox/py27-1.7.X
5 | bin/pip install -U --editable=git+git://github.com/django/django.git@master#egg=django-dev
6 | )
7 | (
8 | cd .tox/py33-1.7.X
9 | bin/pip install -U --editable=git+git://github.com/django/django.git@master#egg=django-dev
10 | )
11 |
--------------------------------------------------------------------------------
/tests/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | setupdir = ..
3 | distribute = False
4 | envlist =
5 | py26-1.4.x,
6 | py26-1.5.x,
7 | py26-1.6.x,
8 | py27-1.4.x,
9 | py27-1.5.x,
10 | py27-1.6.x,
11 | py27-1.7.x,
12 | py33-1.5.x,
13 | py33-1.6.x,
14 | py33-1.7.x,
15 |
16 | [testenv]
17 | downloadcache = {toxworkdir}/_download/
18 | commands =
19 | {envpython} manage.py test {posargs:testapp} --settings=testapp.settings
20 | setenv =
21 | PYTHONPATH = .:{toxworkdir}/../..
22 | FEINCMS_RUN_TESTS = 1
23 |
24 | [testenv:py26-1.4.x]
25 | basepython = python2.6
26 | deps =
27 | django==1.4.7
28 | towel==0.2.0
29 |
30 | [testenv:py27-1.4.x]
31 | basepython = python2.7
32 | deps =
33 | django==1.4.7
34 | towel==0.2.0
35 |
36 | [testenv:py26-1.5.x]
37 | basepython = python2.6
38 | deps =
39 | django==1.5.3
40 | towel==0.2.0
41 |
42 | [testenv:py27-1.5.x]
43 | basepython = python2.7
44 | deps =
45 | django==1.5.3
46 | towel==0.2.0
47 |
48 | [testenv:py26-1.6.x]
49 | basepython = python2.6
50 | deps =
51 | Django==1.6.0
52 | towel==0.2.0
53 |
54 | [testenv:py27-1.6.x]
55 | basepython = python2.7
56 | deps =
57 | Django==1.6.0
58 | towel==0.2.0
59 |
60 | [testenv:py27-1.7.x]
61 | basepython = python2.7
62 | deps =
63 | --editable=git+git://github.com/django/django.git@master#egg=django-dev
64 | towel==0.2.0
65 |
66 | [testenv:py33-1.5.x]
67 | basepython = python3.3
68 | deps =
69 | Django==1.5.4
70 | towel==0.2.0
71 |
72 | [testenv:py33-1.6.x]
73 | basepython = python3.3
74 | deps =
75 | Django==1.6.0
76 | towel==0.2.0
77 |
78 | [testenv:py33-1.7.x]
79 | basepython = python3.3
80 | deps =
81 | --editable=git+git://github.com/django/django.git@master#egg=django-dev
82 | towel==0.2.0
83 |
--------------------------------------------------------------------------------