├── .gitignore ├── AUTHORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── emailauth ├── __init__.py ├── admin.py ├── backends.py ├── forms.py ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── cleanupemailauth.py ├── models.py ├── templates │ └── emailauth │ │ ├── account.html │ │ ├── account_single_email.html │ │ ├── add_email.html │ │ ├── add_email_continue.html │ │ ├── base.html │ │ ├── change_email.html │ │ ├── change_email_continue.html │ │ ├── delete_email.html │ │ ├── logged_out.html │ │ ├── login.html │ │ ├── loginform.html │ │ ├── register.html │ │ ├── register_continue.html │ │ ├── request_password.html │ │ ├── request_password_email.txt │ │ ├── request_password_email_subject.txt │ │ ├── reset_password.html │ │ ├── reset_password_continue.html │ │ ├── set_default_email.html │ │ ├── verification_email.txt │ │ ├── verification_email_subject.txt │ │ └── verify.html ├── templatetags │ ├── __init__.py │ └── emailauth_tags.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── example ├── __init__.py ├── emailauth.db ├── manage.py ├── media │ ├── css │ │ ├── clearfix.css │ │ ├── fancy.css │ │ ├── forms.css │ │ ├── ie6.css │ │ ├── reset.css │ │ ├── site.css │ │ └── typography.css │ └── img │ │ ├── error.png │ │ ├── exclamation_white.png │ │ ├── info.png │ │ ├── question.png │ │ └── success.png ├── middleware.py ├── settings.py ├── settings_singleemail.py ├── templates │ ├── 404.html │ ├── index.html │ └── master.html ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *~ 4 | 5 | # Vim stuff 6 | .vimproject 7 | Session.vim 8 | .ropeproject/ 9 | 10 | # virtualenv stuff 11 | bin/ 12 | include/ 13 | lib/ 14 | build/ 15 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Author of django-emailauth is Vasily Sulatskov 2 | 3 | 4 | The CONTRIBUTORS -- people who have submitted patches, reported bugs and etc.: 5 | 6 | * Ivan Gromov 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Vasily Sulatskov 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include MANIFEST.in 3 | include README.rst 4 | include AUTHORS.txt 5 | recursive-include example *.py *.html *.txt *.css 6 | recursive-include emailauth *.html *.txt 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | Django user email authentication 3 | ================================ 4 | 5 | 6 | This is a Django application providing all essentials for authenticating users 7 | based on email addresses instead of usernames. 8 | 9 | This application can operate in a traditional one user - one email mode as 10 | well as one user - many emails mode. 11 | 12 | This application consists of: 13 | 14 | * UserEmail model 15 | 16 | * Views and forms: 17 | 18 | - Login; 19 | - Password reset; 20 | - Account management: 21 | 22 | - User registration and email confirmation; 23 | - Adding and removing emails to/from existing user accounts; 24 | - Changing default emails 25 | - Changing email (for single email mode) 26 | 27 | * Authentication backends: 28 | 29 | - Email backend for authenticating users who has UserEmail object (regular 30 | site users); 31 | - Fallback backend for users without such objects (most likely that will be 32 | site administration) 33 | 34 | 35 | Motivation for this application 36 | ------------------------------- 37 | 38 | For some reason I was lucky to work on projects which required email-based 39 | authentication, and one of these projects could benefit if users could have 40 | several emails. 41 | 42 | To solve basic authentication problem I quickly came up with this: 43 | http://www.djangosnippets.org/snippets/74/ 44 | 45 | That trick works but it has several drawbacks: 46 | 47 | * It breaks standard Django tests, so when you run python manage.py test on 48 | your project you'll have to filter out error messages from broken Django 49 | tests. Not good. 50 | 51 | * Standard Django login view can't handle long (longer than 30 characters) 52 | email addresses. 53 | 54 | * If you put email verification status and all such into UserProfile class you 55 | tie code working with emails to your project impairing code reuse. 56 | 57 | 58 | To solve above problems I decided to create this application. It stores all 59 | email-specific data into UserEmail model (verification code, code creation 60 | date, verification status etc.) So this application manages all email-related 61 | data, not messing with UserProfile and saves application user from reinventing 62 | the wheel. 63 | 64 | 65 | Example project 66 | --------------- 67 | 68 | To see this application in action:: 69 | 70 | cd emailauth/example 71 | python manage.py syncdb 72 | python manage.py runserver 73 | 74 | Please bear in mind that all emails sent by example project are not actually 75 | sent but printed to stdout instead. 76 | 77 | To see how traditional one user - one email mode works:: 78 | 79 | cd emailauth/example 80 | python manage.py syncdb 81 | python manage.py runserver --settings=settings_singleemail 82 | 83 | 84 | Installation and configuration 85 | ------------------------------ 86 | 87 | Installation 88 | ~~~~~~~~~~~~ 89 | 90 | First you need to somehow obtain 'emailauth' package. 91 | 92 | Place 'emailauth' directory somewhere on your PYHTONPATH, for example in your 93 | project directory, next to your settings.py -- that's the same place where 94 | ``python manage.py startapp`` creates applications. 95 | 96 | If you are using some kind of isolated environment, like virtualenv, you can 97 | just perform a regular installation:: 98 | 99 | python setup.py install 100 | 101 | Or:: 102 | 103 | pip install django-emailauth 104 | 105 | Or:: 106 | 107 | easy_install django-emailauth 108 | 109 | 110 | Configuration 111 | ~~~~~~~~~~~~~ 112 | 113 | Now you need to make several changes to your settings.py 114 | 115 | * Add ``'emailauth'`` to your ``INSTALLED_APPS`` 116 | 117 | * Plug emailauth's authentication backends:: 118 | 119 | AUTHENTICATION_BACKENDS = ( 120 | 'emailauth.backends.EmailBackend', 121 | 'emailauth.backends.FallbackBackend', 122 | ) 123 | 124 | * Configure ``LOGIN_REDIRECT_URL`` and ``LOGIN_URL``. Emailauth's default 125 | urls.py expects them to be like this:: 126 | 127 | LOGIN_REDIRECT_URL = '/account/' 128 | LOGIN_URL = '/login/' 129 | 130 | * Optionally change a life time of email verification codes by changing 131 | ``EMAILAUTH_VERIFICATION_DAYS`` (default value is 3). 132 | 133 | * Optionally set ``EMAILAUTH_USE_SINGLE_EMAIL = False`` if you want to use 134 | emailauth in "multiple-emails mode". 135 | 136 | Now include emailauth's urls.py from your site's urls.py. Of course you may opt 137 | for not including whole emailauth.urls, and write your own configuration, but 138 | if you decide to use the provided urls.py, it will look like this:: 139 | 140 | urlpatterns = patterns('', 141 | (r'', include('emailauth.urls')), 142 | ) 143 | 144 | 145 | Maintenance 146 | ~~~~~~~~~~~ 147 | 148 | By default emailauth uses automatic maintenance - it deletes expired UserEmail 149 | objects when a new unverified email is created. 150 | 151 | If you for some reason want to deactivate it and perform such maintenance 152 | manually you can do it: 153 | 154 | * Set ``EMAILAUTH_USE_AUTOMAINTENANCE = False`` in settings.py 155 | 156 | * Run ``cleanupemailauth`` management command when you want to perform the 157 | cleanup:: 158 | 159 | python manage.py clenupemailauth 160 | 161 | 162 | Template customization 163 | ~~~~~~~~~~~~~~~~~~~~~~ 164 | 165 | Emailauth comes with a set of templates that should get you started, however 166 | they won't be integrated with your site's templates - they don't extend the 167 | right template and use wrong block for main content. 168 | 169 | Don't worry, it's very easy to fix. All emailauth's templates extend 170 | ``emailauth/base.html`` and use ``emailauth_content`` block for content, so all 171 | you need, is to modify ``emailauth/base.html`` and make it extend right 172 | template and place ``emailauth_content`` block into right block specifc to your 173 | site. 174 | 175 | For example if your site's main template is ``mybase.html`` and you place all 176 | content into ``mycontent`` block, you need to make following 177 | ``emailauth/base.html``:: 178 | 179 | {% extends "mybase.html" %} 180 | 181 | {% block mycontent %} 182 | {% block emailauth_content %} 183 | {% endblock %} 184 | {% endblock %} 185 | 186 | 187 | That's all 188 | ~~~~~~~~~~ 189 | 190 | By this point, if you started a new project and followed all the above 191 | instructions above you should have a working instance of emailauth application. 192 | 193 | To test it, start a server:: 194 | 195 | python manage.py runserver 196 | 197 | And open a registration page in your browser: 198 | ``http://127.0.0.1:8000/register/`` - it should display a registration page. 199 | -------------------------------------------------------------------------------- /emailauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/emailauth/__init__.py -------------------------------------------------------------------------------- /emailauth/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from emailauth.models import UserEmail 4 | 5 | 6 | class UserEmailAdmin(admin.ModelAdmin): 7 | model = UserEmail 8 | list_display = ['user', 'email', 'verified',] 9 | 10 | try: 11 | admin.site.register(UserEmail, UserEmailAdmin) 12 | except admin.sites.AlreadyRegistered: 13 | pass 14 | -------------------------------------------------------------------------------- /emailauth/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.auth.backends import ModelBackend 3 | 4 | from emailauth.models import UserEmail 5 | 6 | 7 | class EmailBackend(ModelBackend): 8 | def authenticate(self, username=None, password=None): 9 | try: 10 | email = UserEmail.objects.get(email=username, verified=True) 11 | if email.user.check_password(password): 12 | return email.user 13 | except UserEmail.DoesNotExist: 14 | return None 15 | 16 | 17 | class FallbackBackend(ModelBackend): 18 | def authenticate(self, username=None, password=None): 19 | try: 20 | user = User.objects.get(username=username) 21 | if (user.check_password(password) and 22 | not UserEmail.objects.filter(user=user).count()): 23 | 24 | return user 25 | 26 | except User.DoesNotExist: 27 | return None 28 | -------------------------------------------------------------------------------- /emailauth/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import authenticate 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from django.contrib.auth.models import User 6 | 7 | from emailauth.models import UserEmail 8 | 9 | attrs_dict = {} 10 | 11 | class LoginForm(forms.Form): 12 | email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict))) 13 | password = forms.CharField(widget=forms.PasswordInput(attrs=dict(attrs_dict), 14 | render_value=False)) 15 | 16 | def clean(self): 17 | email = self.cleaned_data.get('email') 18 | password = self.cleaned_data.get('password') 19 | 20 | if email and password: 21 | self.user_cache = authenticate(username=email, password=password) 22 | if self.user_cache is None: 23 | raise forms.ValidationError(_("Please enter a correct email and " 24 | "password. Note that both fields are case-sensitive.")) 25 | elif not self.user_cache.is_active: 26 | raise forms.ValidationError(_("This account is inactive.")) 27 | 28 | return self.cleaned_data 29 | 30 | def get_user_id(self): 31 | if self.user_cache: 32 | return self.user_cache.id 33 | return None 34 | 35 | def get_user(self): 36 | return self.user_cache 37 | 38 | 39 | def get_max_length(model, field_name): 40 | field = model._meta.get_field_by_name(field_name)[0] 41 | return field.max_length 42 | 43 | 44 | def clean_password2(self): 45 | data = self.cleaned_data 46 | if 'password1' in data and 'password2' in data: 47 | if data['password1'] != data['password2']: 48 | raise forms.ValidationError(_( 49 | u'You must type the same password each time.')) 50 | if 'password2' in data: 51 | return data['password2'] 52 | 53 | 54 | class RegistrationForm(forms.Form): 55 | email = forms.EmailField(label=_(u'email address')) 56 | first_name = forms.CharField(label=_(u'first name'), 57 | max_length=get_max_length(User, 'first_name'), 58 | help_text=_(u"That's how we'll call you in emails")) 59 | password1 = forms.CharField(widget=forms.PasswordInput(render_value=False), 60 | label=_(u'password')) 61 | password2 = forms.CharField(widget=forms.PasswordInput(render_value=False), 62 | label=_(u'password (again)')) 63 | 64 | clean_password2 = clean_password2 65 | 66 | def clean_email(self): 67 | email = self.cleaned_data['email'] 68 | 69 | try: 70 | user = UserEmail.objects.get(email=email) 71 | raise forms.ValidationError(_(u'This email is already taken.')) 72 | except UserEmail.DoesNotExist: 73 | pass 74 | return email 75 | 76 | 77 | def save(self): 78 | data = self.cleaned_data 79 | user = User() 80 | user.email = data['email'] 81 | user.first_name = data['name'] 82 | user.set_password(data['password1']) 83 | user.save() 84 | 85 | desired_username = 'id_%d_%s' % (user.id, user.email) 86 | user.username = desired_username[:get_max_length(User, 'username')] 87 | user.is_active = False 88 | user.save() 89 | 90 | registration_profile = ( 91 | RegistrationProfile.objects.create_inactive_profile(user)) 92 | registration_profile.save() 93 | 94 | profile = Account() 95 | profile.user = user 96 | profile.save() 97 | 98 | return user, registration_profile 99 | 100 | 101 | class PasswordResetRequestForm(forms.Form): 102 | email = forms.EmailField(label=_(u'your email address')) 103 | 104 | def clean_email(self): 105 | data = self.cleaned_data 106 | try: 107 | user_email = UserEmail.objects.get(email=data['email']) 108 | return data['email'] 109 | except UserEmail.DoesNotExist: 110 | raise forms.ValidationError(_(u'Unknown email')) 111 | 112 | 113 | class PasswordResetForm(forms.Form): 114 | password1 = forms.CharField(widget=forms.PasswordInput(render_value=False), 115 | label=_(u'password')) 116 | password2 = forms.CharField(widget=forms.PasswordInput(render_value=False), 117 | label=_(u'password (again)')) 118 | 119 | clean_password2 = clean_password2 120 | 121 | 122 | class AddEmailForm(forms.Form): 123 | email = forms.EmailField(label=_(u'new email address')) 124 | 125 | def clean_email(self): 126 | email = self.cleaned_data['email'] 127 | 128 | try: 129 | user = UserEmail.objects.get(email=email) 130 | raise forms.ValidationError(_(u'This email is already taken.')) 131 | except UserEmail.DoesNotExist: 132 | pass 133 | return email 134 | 135 | 136 | class DeleteEmailForm(forms.Form): 137 | yes = forms.BooleanField(required=True) 138 | 139 | def __init__(self, user, *args, **kwds): 140 | self.user = user 141 | super(DeleteEmailForm, self).__init__(*args, **kwds) 142 | 143 | def clean(self): 144 | count = UserEmail.objects.filter(user=self.user).count() 145 | if UserEmail.objects.filter(user=self.user, verified=True).count() < 2: 146 | raise forms.ValidationError(_('You can not delete your last verified ' 147 | 'email.')) 148 | return self.cleaned_data 149 | 150 | 151 | class ConfirmationForm(forms.Form): 152 | yes = forms.BooleanField(required=True) 153 | -------------------------------------------------------------------------------- /emailauth/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/emailauth/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /emailauth/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Russian translation 2 | # This file is distributed under the same license as the django-emailauth package. 3 | # Ivan Gromov 2009 4 | # 5 | #, fuzzy 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2009-07-09 18:41+0600\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Ivan Gromov \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | 17 | #: forms.py:23 18 | msgid "" 19 | "Please enter a correct email and password. Note that both fields are case-" 20 | "sensitive." 21 | msgstr "" 22 | "Введите правильные электронный адрес и пароль. Оба поля чувствительны к " 23 | "регистру." 24 | 25 | #: forms.py:26 26 | msgid "This account is inactive." 27 | msgstr "Аккаунт заблокирован" 28 | 29 | #: forms.py:49 30 | msgid "You must type the same password each time." 31 | msgstr "Пароль и подтверждение должны совпадать" 32 | 33 | #: forms.py:55 34 | msgid "email address" 35 | msgstr "Электронный адрес" 36 | 37 | #: forms.py:56 38 | msgid "first name" 39 | msgstr "Имя" 40 | 41 | #: forms.py:58 42 | msgid "That's how we'll call you in emails" 43 | msgstr "Как к Вам обращаться в письмах" 44 | 45 | #: forms.py:60 forms.py:115 46 | msgid "password" 47 | msgstr "Пароль" 48 | 49 | #: forms.py:62 forms.py:117 50 | msgid "password (again)" 51 | msgstr "Подтверждение пароля" 52 | 53 | #: forms.py:71 forms.py:130 54 | msgid "This email is already taken." 55 | msgstr "Этот адрес уже используется" 56 | 57 | #: forms.py:102 58 | msgid "your email address" 59 | msgstr "Ваш электронный адрес" 60 | 61 | #: forms.py:110 62 | msgid "Unknown email" 63 | msgstr "Электронный адрес не зарегистрирован" 64 | 65 | #: forms.py:123 66 | msgid "new email address" 67 | msgstr "Новый электронный адрес" 68 | 69 | #: forms.py:146 70 | msgid "You can not delete your last verified email." 71 | msgstr "Нельзя удалять последний подтвержденный электронный адррес" 72 | 73 | #: models.py:59 74 | msgid "user email" 75 | msgstr "Электронный адрес пользователя" 76 | 77 | #: models.py:60 78 | msgid "user emails" 79 | msgstr "Электронные адреса пользователя" 80 | 81 | #: models.py:66 82 | msgid "user" 83 | msgstr "Пользователь" 84 | 85 | #: models.py:71 86 | msgid "verification key" 87 | msgstr "Код подтверждения" 88 | 89 | #: views.py:61 90 | msgid "" 91 | "Your Web browser doesn't appear to have cookies enabled. Cookies are " 92 | "required for logging in." 93 | msgstr "" 94 | "Ваш браузер не поддерживает cookies. Для входа в системуcookies должны быть " 95 | "включены" 96 | 97 | #: views.py:177 98 | #, python-format 99 | msgid "%s email confirmed." 100 | msgstr "Электронный адрес %s подтвержден." 101 | 102 | #: templates/emailauth/account.html:4 103 | #: templates/emailauth/account_single_email.html:4 104 | msgid "Account properties" 105 | msgstr "Свойства профиля" 106 | 107 | #: templates/emailauth/account.html:7 108 | #: templates/emailauth/account_single_email.html:7 109 | msgid "Username:" 110 | msgstr "Пользователь:" 111 | 112 | #: templates/emailauth/account.html:11 113 | #: templates/emailauth/account_single_email.html:11 114 | msgid "First name:" 115 | msgstr "Имя:" 116 | 117 | #: templates/emailauth/account.html:15 118 | msgid "Default email:" 119 | msgstr "Электронный адрес по умолчанию" 120 | 121 | #: templates/emailauth/account.html:19 122 | msgid "Extra emails" 123 | msgstr "Дополнительные адреса" 124 | 125 | #: templates/emailauth/account.html:25 126 | msgid "Set as default" 127 | msgstr "Установить по умоллчанию" 128 | 129 | #: templates/emailauth/account.html:26 130 | msgid "Delete" 131 | msgstr "Удалить" 132 | 133 | #: templates/emailauth/account.html:32 134 | msgid "Unverified emails" 135 | msgstr "Неподтвержденные адерса" 136 | 137 | #: templates/emailauth/account.html:36 138 | msgid "resend verification email" 139 | msgstr "Отправить подтверждение ещё раз" 140 | 141 | #: templates/emailauth/account.html:41 142 | msgid "Add email" 143 | msgstr "Добавить электронный адрес" 144 | 145 | #: templates/emailauth/account_single_email.html:15 146 | msgid "Email:" 147 | msgstr "Электронный адрес:" 148 | 149 | #: templates/emailauth/account_single_email.html:19 150 | msgid "Change email" 151 | msgstr "Изменить электронный адрес" 152 | 153 | #: templates/emailauth/add_email.html:4 154 | msgid "Add another email to your account" 155 | msgstr "Добавтиь ещё один электронный адрес к профилю" 156 | 157 | #: templates/emailauth/add_email.html:9 158 | #: templates/emailauth/change_email.html:9 159 | #: templates/emailauth/request_password.html:9 160 | #: templates/emailauth/reset_password.html:9 161 | msgid "submit" 162 | msgstr "Отправить" 163 | 164 | #: templates/emailauth/change_email.html:4 165 | msgid "Set new email address" 166 | msgstr "Установить новый электронный адрес" 167 | 168 | #: templates/emailauth/delete_email.html:4 169 | msgid "Delete your email" 170 | msgstr "Удалить электронный адрес" 171 | 172 | #: templates/emailauth/delete_email.html:6 173 | msgid "Yes" 174 | msgstr "Да" 175 | 176 | #: templates/emailauth/delete_email.html:7 177 | #: templates/emailauth/set_default_email.html:7 178 | msgid "No" 179 | msgstr "Нет" 180 | 181 | #: templates/emailauth/logged_out.html:4 182 | msgid "You are logged out!" 183 | msgstr "Вы вышли из системы" 184 | 185 | #: templates/emailauth/login.html:4 templates/emailauth/login.html.py:9 186 | #: templates/emailauth/loginform.html:12 187 | msgid "Login" 188 | msgstr "Вход в систему" 189 | 190 | #: templates/emailauth/login.html:11 templates/emailauth/loginform.html:14 191 | #: templates/emailauth/request_password.html:4 192 | msgid "Forgot your password?" 193 | msgstr "Забыли пароль?" 194 | 195 | #: templates/emailauth/loginform.html:4 196 | msgid "Welcome" 197 | msgstr "Добро пожаловать" 198 | 199 | #: templates/emailauth/loginform.html:5 200 | msgid "Account" 201 | msgstr "Профиль" 202 | 203 | #: templates/emailauth/loginform.html:6 204 | msgid "Logout" 205 | msgstr "Выход" 206 | 207 | #: templates/emailauth/loginform.html:13 templates/emailauth/register.html:10 208 | msgid "Register" 209 | msgstr "Зарегистрироваться" 210 | 211 | #: templates/emailauth/register.html:5 212 | msgid "Registration" 213 | msgstr "Регистрация" 214 | 215 | #: templates/emailauth/reset_password.html:4 216 | msgid "Set new password" 217 | msgstr "Сменить пароль" 218 | 219 | #: templates/emailauth/reset_password_continue.html:4 220 | msgid "Confirm your email" 221 | msgstr "Подтверждение электронного адреса" 222 | 223 | #: templates/emailauth/reset_password_continue.html:5 224 | #, python-format 225 | msgid "" 226 | "\n" 227 | " We've sent an email to %(email)s containing a link\n" 229 | " you'll need to follow to reset your password. You should receive " 230 | "the\n" 231 | " email within the next few minutes.\n" 232 | " " 233 | msgstr "" 234 | "\n" 235 | " На Ваш адрес %(email)s " 236 | "мы отправили письмо, содержащее ссылку с кодом \n" 237 | " для сброса пароля. Письмо будет доставлено \n" 238 | " в течение нескольких минут.\n" 239 | " " 240 | 241 | #: templates/emailauth/reset_password_continue.html:11 242 | msgid "Didn't get the email?" 243 | msgstr "Не получили письмо?" 244 | 245 | #: templates/emailauth/reset_password_continue.html:13 246 | msgid "" 247 | "Below are some of the most common reasons you might not be getting the " 248 | "message:" 249 | msgstr "Это могло произойти по следующим причинам:" 250 | 251 | #: templates/emailauth/reset_password_continue.html:15 252 | msgid "First, be patient, sometimes it takes a while for the email to arrive." 253 | msgstr "Во-первых, будьте терпеливы, иногда письмо может задержаться в пути" 254 | 255 | #: templates/emailauth/reset_password_continue.html:16 256 | msgid "Check above to ensure that you entered your email address correctly." 257 | msgstr "Проверьте правильность ввода электронного адреса" 258 | 259 | #: templates/emailauth/reset_password_continue.html:17 260 | msgid "" 261 | "Check your junk email box, the message might have been filtered as junk." 262 | msgstr "Проверьте, не было ли письмо отфильтровано как спам" 263 | 264 | #: templates/emailauth/set_default_email.html:4 265 | #, python-format 266 | msgid "Set %(email)s as your new default email?" 267 | msgstr "Установить адрес %(email)s как основной?" 268 | 269 | #: templates/emailauth/verify.html:5 270 | msgid "Verified" 271 | msgstr "Подтверждено" 272 | 273 | #: templates/emailauth/verify.html:7 274 | msgid "Not verified" 275 | msgstr "Электронный адрес не подтвержден" 276 | -------------------------------------------------------------------------------- /emailauth/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/emailauth/management/__init__.py -------------------------------------------------------------------------------- /emailauth/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/emailauth/management/commands/__init__.py -------------------------------------------------------------------------------- /emailauth/management/commands/cleanupemailauth.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | from emailauth.models import UserEmail 4 | 5 | 6 | class Command(NoArgsCommand): 7 | help = "Delete expired UserEmail objects from the database" 8 | 9 | def handle_noargs(self, **options): 10 | UserEmail.objects.delete_expired() 11 | -------------------------------------------------------------------------------- /emailauth/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | 4 | import django.core.mail 5 | 6 | from django.db import models 7 | from django.contrib.auth.models import User 8 | from django.contrib.sites.models import Site 9 | 10 | from django.template.loader import render_to_string 11 | 12 | from django.utils.hashcompat import sha_constructor 13 | from django.utils.translation import ugettext_lazy as _ 14 | import django.core.mail 15 | 16 | from django.conf import settings 17 | 18 | from emailauth.utils import email_verification_days, use_automaintenance 19 | 20 | class UserEmailManager(models.Manager): 21 | def make_random_key(self, email): 22 | salt = sha_constructor(str(random.random())).hexdigest()[:5] 23 | key = sha_constructor(salt + email).hexdigest() 24 | return key 25 | 26 | def create_unverified_email(self, email, user=None): 27 | if use_automaintenance(): 28 | self.delete_expired() 29 | 30 | email_obj = UserEmail(email=email, user=user, default=user is None, 31 | verification_key=self.make_random_key(email)) 32 | return email_obj 33 | 34 | def verify(self, verification_key): 35 | try: 36 | email = self.get(verification_key=verification_key) 37 | except self.model.DoesNotExist: 38 | return None 39 | if not email.verification_key_expired(): 40 | email.verification_key = self.model.VERIFIED 41 | email.verified = True 42 | email.save() 43 | return email 44 | 45 | def delete_expired(self): 46 | date_threshold = (datetime.datetime.now() - 47 | datetime.timedelta(days=email_verification_days())) 48 | expired_emails = self.filter(code_creation_date__lt=date_threshold) 49 | 50 | for email in expired_emails: 51 | if not email.verified: 52 | user = email.user 53 | emails = user.useremail_set.all() 54 | if not user.is_active: 55 | user.delete() 56 | else: 57 | email.delete() 58 | 59 | 60 | class UserEmail(models.Model): 61 | class Meta: 62 | verbose_name = _('user email') 63 | verbose_name_plural = _('user emails') 64 | 65 | VERIFIED = 'ALREADY_VERIFIED' 66 | 67 | objects = UserEmailManager() 68 | 69 | user = models.ForeignKey(User, null=True, blank=True, verbose_name=_('user')) 70 | default = models.BooleanField(default=False) 71 | email = models.EmailField(unique=True) 72 | verified = models.BooleanField(default=False) 73 | code_creation_date = models.DateTimeField(default=datetime.datetime.now) 74 | verification_key = models.CharField(_('verification key'), max_length=40) 75 | 76 | def __init__(self, *args, **kwds): 77 | super(UserEmail, self).__init__(*args, **kwds) 78 | self._original_default = self.default 79 | 80 | def __unicode__(self): 81 | return self.email 82 | 83 | def save(self, *args, **kwds): 84 | super(UserEmail, self).save(*args, **kwds) 85 | if self.default and not self._original_default: 86 | self.user.email = self.email 87 | self.user.save() 88 | for email in self.__class__.objects.filter(user=self.user): 89 | if email.id != self.id and email.default: 90 | email.default = False 91 | email.save() 92 | 93 | def make_new_key(self): 94 | self.verification_key = self.__class__.objects.make_random_key( 95 | self.email) 96 | self.code_creation_date = datetime.datetime.now() 97 | 98 | def send_verification_email(self, first_name=None): 99 | current_site = Site.objects.get_current() 100 | 101 | subject = render_to_string('emailauth/verification_email_subject.txt', 102 | {'site': current_site}) 103 | # Email subject *must not* contain newlines 104 | subject = ''.join(subject.splitlines()) 105 | 106 | emails = set() 107 | if self.user is not None: 108 | for email in self.__class__.objects.filter(user=self.user): 109 | emails.add(email.email) 110 | emails.add(self.email) 111 | first_email = len(emails) == 1 112 | 113 | if first_name is None: 114 | first_name = self.user.first_name 115 | 116 | message = render_to_string('emailauth/verification_email.txt', { 117 | 'verification_key': self.verification_key, 118 | 'expiration_days': email_verification_days(), 119 | 'site': current_site, 120 | 'first_name': first_name, 121 | 'first_email': first_email, 122 | }) 123 | 124 | self.code_creation_date = datetime.datetime.now() 125 | 126 | django.core.mail.send_mail(subject, message, 127 | settings.DEFAULT_FROM_EMAIL, [self.email]) 128 | 129 | 130 | def verification_key_expired(self): 131 | expiration_date = datetime.timedelta(days=email_verification_days()) 132 | return (self.verification_key == self.VERIFIED or 133 | (self.code_creation_date + expiration_date <= datetime.datetime.now())) 134 | 135 | verification_key_expired.boolean = True 136 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/account.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Account properties' %}

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
{% trans 'First name:' %}{{ user.first_name }}
{% trans 'Default email:' %}{{ user.email }}
15 |

{% trans 'Extra emails' %}

16 | {% if extra_emails %} 17 | 26 | {% endif %} 27 | {% if unverified_emails %} 28 |

{% trans 'Unverified emails' %}

29 | 36 | {% endif %} 37 | {% trans 'Add email' %} 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/account_single_email.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Account properties' %}

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
{% trans 'First name:' %}{{ user.first_name }}
{% trans 'Email:'%}{{ user.email }}
15 | {% trans 'Change email' %} 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/add_email.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Add another email to your account' %}

5 |
6 | 7 | {{ form }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/add_email_continue.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/register_continue.html" %} 2 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/base.html: -------------------------------------------------------------------------------- 1 | {# Customize this template as needed for integration with your site's templates. #} 2 | 3 | {# Default template integrates with emailauth's example project. #} 4 | 5 | {% extends "master.html" %} 6 | 7 | {% block content %} 8 | {% block emailauth_content %} 9 | {% endblock %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/change_email.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Set new email address' %}

5 |
6 | 7 | {{ form }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/change_email_continue.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/register_continue.html" %} 2 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/delete_email.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Delete your email' %} {{ email }}?

5 |
6 | 7 | {% trans 'No' %} 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'You are logged out!' %}

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Login' %}

5 |
6 | 7 | {{ form }} 8 |
9 | 10 |
11 | {% trans 'Forgot your password?' %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/loginform.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | {% if user.is_authenticated %} 4 | {% trans 'Welcome' %}, {{ user.first_name }}! 5 | {% trans 'Account' %} 6 | {% trans 'Logout' %} 7 | {% else %} 8 |
9 | 10 | {{ form }} 11 |
12 | 13 | {% trans 'Register' %} 14 | {% trans 'Forgot your password?' %} 15 |
16 | {% endif %} 17 |
18 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Registration' %}

5 |
6 | 7 | {{ form.as_table }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/register_continue.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/reset_password_continue.html" %} -------------------------------------------------------------------------------- /emailauth/templates/emailauth/request_password.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Forgot your password?' %}

5 |
6 | 7 | {{ form }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/request_password_email.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% blocktrans %}Dear {{ first_name }},{% endblocktrans %} 2 | 3 | You (or someone posing as you) has requested a password reset for your account on {{ site.name }} 4 | 5 | To set new password please follow this link: 6 | http://{{ site.domain }}{% url emailauth_reset_password reset_code %} 7 | 8 | If you hasn't request a password reset, just ignore this email and the link above will expire in {{ expiration_days }} days. 9 | 10 | {# vim: set ft=django: #} 11 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/request_password_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}[{{ site }}]{% trans 'Password reset' %} 2 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Set new password' %}

5 |
6 | 7 | {{ form }} 8 |
9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/reset_password_continue.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% trans 'Confirm your email' %}

5 |

{% blocktrans %} 6 | We've sent an email to {{ email }} containing a link 7 | you'll need to follow to reset your password. You should receive the 8 | email within the next few minutes. 9 | {% endblocktrans %} 10 |

11 |

{% trans "Didn't get the email?" %}

12 |

13 | {% trans 'Below are some of the most common reasons you might not be getting the message:' %} 14 |

    15 |
  • {% trans 'First, be patient, sometimes it takes a while for the email to arrive.' %}
  • 16 |
  • {% trans 'Check above to ensure that you entered your email address correctly.' %}
  • 17 |
  • {% trans 'Check your junk email box, the message might have been filtered as junk.' %}
  • 18 |
19 |

20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/set_default_email.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 |

{% blocktrans %}Set {{ email }} as your new default email?{% endblocktrans %}

5 |
6 | 7 | {% trans 'No' %} 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/verification_email.txt: -------------------------------------------------------------------------------- 1 | Dear {{ first_name }}, 2 | 3 | {% if first_email %} 4 | To activate your account on {{ site.name }} please follow this link:{% else %}To verify your email for your account on {{ site.name }} please follow this link:{% endif %} 5 | http://{{ site.domain }}{% url emailauth_verify verification_key %} 6 | The link above will expire in {{ expiration_days }} days. 7 | {# vim: set ft=django: #} 8 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/verification_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}[{{ site }}]{% trans 'Please confirm your email' %} 2 | -------------------------------------------------------------------------------- /emailauth/templates/emailauth/verify.html: -------------------------------------------------------------------------------- 1 | {% extends "emailauth/base.html" %}{% load i18n %} 2 | 3 | {% block emailauth_content %} 4 | {% if email %} 5 |

{% trans 'Verified' %}

6 | {% else %} 7 |

{% trans 'Not verified' %}

8 | {% endif %} 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /emailauth/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/emailauth/templatetags/__init__.py -------------------------------------------------------------------------------- /emailauth/templatetags/emailauth_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django import template 3 | from emailauth.forms import LoginForm 4 | 5 | register = template.Library() 6 | 7 | @register.inclusion_tag('emailauth/loginform.html', takes_context=True) 8 | def loginform(context): 9 | form = LoginForm() 10 | user = context['request'].user 11 | return locals() -------------------------------------------------------------------------------- /emailauth/tests.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timedelta 3 | 4 | from django.test.client import Client 5 | from django.test.testcases import TestCase 6 | from django.core import mail 7 | from django.contrib.auth.models import User 8 | from django.conf import settings 9 | 10 | from emailauth.models import UserEmail 11 | from emailauth.utils import email_verification_days 12 | 13 | 14 | class Status: 15 | OK = 200 16 | REDIRECT = 302 17 | NOT_FOUND = 404 18 | 19 | 20 | class BaseTestCase(TestCase): 21 | def assertStatusCode(self, response, status_code=200): 22 | self.assertEqual(response.status_code, status_code) 23 | 24 | def checkSimplePage(self, path, params={}): 25 | client = Client() 26 | response = client.get(path, params) 27 | self.assertStatusCode(response) 28 | 29 | def createActiveUser(self, username='username', email='user@example.com', 30 | password='password'): 31 | 32 | user = User(username=username, email=email, is_active=True) 33 | user.first_name = 'John' 34 | user.set_password(password) 35 | user.save() 36 | 37 | user_email = UserEmail(user=user, email=email, verified=True, 38 | default=True, verification_key=UserEmail.VERIFIED) 39 | user_email.save() 40 | return user, user_email 41 | 42 | def getLoggedInClient(self, email='user@example.com', password='password'): 43 | client = Client() 44 | client.login(username=email, password=password) 45 | return client 46 | 47 | 48 | class RegisterTest(BaseTestCase): 49 | def testRegisterGet(self): 50 | self.checkSimplePage('/register/') 51 | 52 | def testRegisterPost(self): 53 | client = Client() 54 | response = client.post('/register/', { 55 | 'email': 'user@example.com', 56 | 'first_name': 'John', 57 | 'password1': 'password', 58 | 'password2': 'password', 59 | }) 60 | self.assertRedirects(response, '/register/continue/user%40example.com/') 61 | 62 | self.assertEqual(len(mail.outbox), 1) 63 | 64 | email = mail.outbox[0] 65 | 66 | addr_re = re.compile(r'.*http://.*?(/\S*/)', re.UNICODE | re.MULTILINE) 67 | verification_url = addr_re.search(email.body).groups()[0] 68 | 69 | response = client.get(verification_url) 70 | 71 | self.assertRedirects(response, '/account/') 72 | 73 | response = client.post('/login/', { 74 | 'email': 'user@example.com', 75 | 'password': 'password', 76 | }) 77 | 78 | self.assertRedirects(response, '/account/') 79 | 80 | user = User.objects.get(email='user@example.com') 81 | self.assertEqual(user.first_name, 'John') 82 | 83 | def testRegisterSame(self): 84 | user, user_email = self.createActiveUser() 85 | client = Client() 86 | response = client.post('/register/', { 87 | 'email': user_email.email, 88 | 'first_name': 'John', 89 | 'password1': 'password', 90 | 'password2': 'password', 91 | }) 92 | self.assertContains(response, 'This email is already taken') 93 | 94 | email_obj = UserEmail.objects.create_unverified_email( 95 | 'user@example.org', user) 96 | email_obj.save() 97 | 98 | response = client.post('/register/', { 99 | 'email': 'user@example.org', 100 | 'first_name': 'John', 101 | 'password1': 'password', 102 | 'password2': 'password', 103 | }) 104 | self.assertContains(response, 'This email is already taken') 105 | 106 | 107 | 108 | class LoginTest(BaseTestCase): 109 | def testLoginGet(self): 110 | self.checkSimplePage('/login/') 111 | 112 | def testLoginFail(self): 113 | user, user_email = self.createActiveUser() 114 | client = Client() 115 | response = client.post('/login/', { 116 | 'email': 'user@example.com', 117 | 'password': 'wrongpassword', 118 | }) 119 | self.assertStatusCode(response, Status.OK) 120 | 121 | 122 | class PasswordResetTest(BaseTestCase): 123 | def prepare(self): 124 | user, user_email = self.createActiveUser() 125 | 126 | client = Client() 127 | response = client.post('/resetpassword/', { 128 | 'email': user_email.email, 129 | }) 130 | 131 | self.assertRedirects(response, 132 | '/resetpassword/continue/user%40example.com/') 133 | 134 | email = mail.outbox[0] 135 | addr_re = re.compile(r'.*http://.*?(/\S*/)', re.UNICODE | re.MULTILINE) 136 | reset_url = addr_re.search(email.body).groups()[0] 137 | return reset_url, user_email 138 | 139 | 140 | def testPasswordReset(self): 141 | reset_url, user_email = self.prepare() 142 | client = Client() 143 | 144 | self.checkSimplePage(reset_url) 145 | 146 | response = client.post(reset_url, { 147 | 'password1': 'newpassword', 148 | 'password2': 'newpassword', 149 | }) 150 | 151 | self.assertRedirects(response, '/account/') 152 | 153 | user = User.objects.get(email=user_email.email) 154 | self.assertTrue(user.check_password('newpassword')) 155 | 156 | response = client.get(reset_url) 157 | self.assertStatusCode(response, Status.NOT_FOUND) 158 | 159 | 160 | def testPasswordResetFail(self): 161 | reset_url, user_email = self.prepare() 162 | client = Client() 163 | user_email.verification_key = UserEmail.VERIFIED 164 | user_email.save() 165 | 166 | response = client.get(reset_url) 167 | self.assertStatusCode(response, Status.NOT_FOUND) 168 | 169 | 170 | def testPasswordResetFail2(self): 171 | reset_url, user_email = self.prepare() 172 | client = Client() 173 | user_email.code_creation_date = (datetime.now() - 174 | timedelta(days=email_verification_days() + 1)) 175 | user_email.save() 176 | 177 | response = client.get(reset_url) 178 | self.assertStatusCode(response, Status.NOT_FOUND) 179 | 180 | 181 | class TestAddEmail(BaseTestCase): 182 | def setUp(self): 183 | self.user, self.user_email = self.createActiveUser() 184 | self.client = self.getLoggedInClient() 185 | 186 | def testAddEmailGet(self): 187 | response = self.client.get('/account/addemail/') 188 | self.assertStatusCode(response, Status.OK) 189 | 190 | def testAddEmail(self): 191 | response = self.client.post('/account/addemail/', { 192 | 'email': 'user@example.org', 193 | }) 194 | self.assertRedirects(response, '/account/addemail/continue/user%40example.org/') 195 | 196 | self.assertEqual(len(mail.outbox), 1) 197 | 198 | email = mail.outbox[0] 199 | 200 | addr_re = re.compile(r'.*http://.*?(/\S*/)', re.UNICODE | re.MULTILINE) 201 | verification_url = addr_re.search(email.body).groups()[0] 202 | 203 | response = self.client.get(verification_url) 204 | 205 | self.assertRedirects(response, '/account/') 206 | 207 | client = Client() 208 | response = client.post('/login/', { 209 | 'email': 'user@example.org', 210 | 'password': 'password', 211 | }) 212 | 213 | self.assertRedirects(response, '/account/') 214 | 215 | def testAddSameEmail(self): 216 | response = self.client.post('/account/addemail/', { 217 | 'email': 'user@example.com', 218 | }) 219 | self.assertStatusCode(response, Status.OK) 220 | 221 | response = self.client.post('/account/addemail/', { 222 | 'email': 'user@example.org', 223 | }) 224 | self.assertRedirects(response, 225 | '/account/addemail/continue/user%40example.org/') 226 | 227 | response = self.client.post('/account/addemail/', { 228 | 'email': 'user@example.org', 229 | }) 230 | self.assertStatusCode(response, Status.OK) 231 | 232 | 233 | class TestDeleteEmail(BaseTestCase): 234 | def setUp(self): 235 | self.user, self.user_email = self.createActiveUser() 236 | self.client = self.getLoggedInClient() 237 | 238 | def testDeleteEmail(self): 239 | user = self.user 240 | user_email = UserEmail(user=user, email='email@example.org', verified=True, 241 | default=False, verification_key=UserEmail.VERIFIED) 242 | user_email.save() 243 | 244 | response = self.client.post('/account/deleteemail/%s/' % user_email.id, { 245 | 'yes': 'yes', 246 | }) 247 | 248 | self.assertRedirects(response, '/account/') 249 | 250 | user_emails = UserEmail.objects.filter(user=self.user) 251 | self.assertEqual(len(user_emails), 1) 252 | 253 | response = self.client.post('/account/deleteemail/%s/' % user_emails[0].id, { 254 | 'yes': 'yes', 255 | }) 256 | 257 | self.assertStatusCode(response, Status.OK) 258 | 259 | 260 | class TestSetDefaultEmail(BaseTestCase): 261 | def setUp(self): 262 | self.user, self.user_email = self.createActiveUser() 263 | self.client = self.getLoggedInClient() 264 | 265 | def testSetDefaultEmailGet(self): 266 | response = self.client.get('/account/setdefaultemail/%s/' % 267 | self.user_email.id) 268 | self.assertStatusCode(response, Status.OK) 269 | 270 | def testSetDefaultEmail(self): 271 | user = self.user 272 | user_email = UserEmail(user=user, email='user@example.org', verified=True, 273 | default=False, verification_key=UserEmail.VERIFIED) 274 | user_email.save() 275 | 276 | response = self.client.post('/account/setdefaultemail/%s/' % user_email.id, { 277 | 'yes': 'yes', 278 | }) 279 | 280 | self.assertRedirects(response, '/account/') 281 | 282 | new_default_email = user_email.email 283 | 284 | for email in UserEmail.objects.filter(): 285 | self.assertEqual(email.default, email.email == new_default_email) 286 | 287 | user = User.objects.get(id=self.user.id) 288 | self.assertEqual(user.email, new_default_email) 289 | 290 | def testSetDefaultUnverifiedEmail(self): 291 | user = self.user 292 | user_email = UserEmail(user=user, email='user@example.org', verified=False, 293 | default=False, verification_key=UserEmail.VERIFIED) 294 | user_email.save() 295 | 296 | response = self.client.post('/account/setdefaultemail/%s/' % user_email.id, { 297 | 'yes': 'yes', 298 | }) 299 | self.assertStatusCode(response, Status.NOT_FOUND) 300 | 301 | class TestDeleteEmail(BaseTestCase): 302 | def setUp(self): 303 | self.user, self.user_email = self.createActiveUser() 304 | self.client = self.getLoggedInClient() 305 | 306 | def testDeleteEmail(self): 307 | user_email = UserEmail(user=self.user, email='user@example.org', verified=True, 308 | default=False, verification_key=UserEmail.VERIFIED) 309 | user_email.save() 310 | 311 | page_url = '/account/deleteemail/%s/' % user_email.id 312 | 313 | response = self.client.get(page_url) 314 | self.assertStatusCode(response, Status.OK) 315 | 316 | response = self.client.post(page_url, {'yes': 'yes'}) 317 | self.assertRedirects(response, '/account/') 318 | 319 | def testDeleteUnverifiedEmail(self): 320 | user_email = UserEmail(user=self.user, email='user@example.org', verified=False, 321 | default=False, verification_key=UserEmail.VERIFIED) 322 | user_email.save() 323 | 324 | response = self.client.post('/account/deleteemail/%s/' % user_email.id, { 325 | 'yes': 'yes', 326 | }) 327 | self.assertStatusCode(response, Status.NOT_FOUND) 328 | 329 | 330 | class TestAccountSingleEmail(BaseTestCase): 331 | def setUp(self): 332 | self.user, self.user_email = self.createActiveUser() 333 | self.client = self.getLoggedInClient() 334 | settings.EMAILAUTH_USE_SINGLE_EMAIL = True 335 | 336 | def tearDown(self): 337 | settings.EMAILAUTH_USE_SINGLE_EMAIL = False 338 | 339 | def testAccountGet(self): 340 | response = self.client.get('/account/') 341 | self.assertStatusCode(response, Status.OK) 342 | 343 | class TestChangeEmail(BaseTestCase): 344 | def setUp(self): 345 | self.user, self.user_email = self.createActiveUser() 346 | self.client = self.getLoggedInClient() 347 | settings.EMAILAUTH_USE_SINGLE_EMAIL = True 348 | 349 | def tearDown(self): 350 | settings.EMAILAUTH_USE_SINGLE_EMAIL = False 351 | 352 | def testEmailChangeWrongMode(self): 353 | settings.EMAILAUTH_USE_SINGLE_EMAIL = False 354 | response = self.client.get('/account/changeemail/') 355 | self.assertStatusCode(response, Status.NOT_FOUND) 356 | 357 | def testEmailChange(self): 358 | response = self.client.get('/account/changeemail/') 359 | self.assertStatusCode(response, Status.OK) 360 | 361 | response = self.client.post('/account/changeemail/', { 362 | 'email': 'user@example.org', 363 | }) 364 | 365 | self.assertRedirects(response, 366 | '/account/changeemail/continue/user%40example.org/') 367 | 368 | self.assertEqual(len(mail.outbox), 1) 369 | 370 | email = mail.outbox[0] 371 | 372 | addr_re = re.compile(r'.*http://.*?(/\S*/)', re.UNICODE | re.MULTILINE) 373 | verification_url = addr_re.search(email.body).groups()[0] 374 | 375 | response = self.client.get(verification_url) 376 | 377 | self.assertRedirects(response, '/account/') 378 | 379 | client = Client() 380 | response = client.post('/login/', { 381 | 'email': 'user@example.org', 382 | 'password': 'password', 383 | }) 384 | 385 | self.assertRedirects(response, '/account/') 386 | 387 | user = User.objects.get(id=self.user.id) 388 | self.assertEqual(user.email, 'user@example.org') 389 | 390 | client = Client() 391 | response = client.post('/login/', { 392 | 'email': 'user@example.com', 393 | 'password': 'password', 394 | }) 395 | self.assertStatusCode(response, Status.OK) 396 | 397 | 398 | class TestResendEmail(BaseTestCase): 399 | def setUp(self): 400 | self.user, self.user_email = self.createActiveUser() 401 | self.client = self.getLoggedInClient() 402 | 403 | def testResendEmail(self): 404 | user = self.user 405 | user_email = UserEmail(user=user, email='user@example.org', verified=False, 406 | default=False, verification_key='abcdef') 407 | user_email.save() 408 | 409 | response = self.client.get('/account/resendemail/%s/' % user_email.id) 410 | self.assertRedirects(response, 411 | '/account/addemail/continue/user%40example.org/') 412 | self.assertEqual(len(mail.outbox), 1) 413 | 414 | 415 | class TestCleanup(BaseTestCase): 416 | def testCleanup(self): 417 | user1 = User(username='user1', email='user1@example.com', is_active=True) 418 | user1.save() 419 | 420 | old_enough = (datetime.now() - timedelta(days=email_verification_days() + 1)) 421 | not_old_enough = (datetime.now() - 422 | timedelta(days=email_verification_days() - 1)) 423 | 424 | email1 = UserEmail(user=user1, email='user1@example.com', 425 | verified=True, default=True, 426 | verification_key=UserEmail.VERIFIED + 'asd', 427 | code_creation_date=old_enough) 428 | email1.save() 429 | 430 | user2 = User(username='user2', email='user2@example.com', is_active=False) 431 | user2.save() 432 | 433 | email2 = UserEmail(user=user2, email='user2@example.com', 434 | verified=False, default=True, 435 | verification_key='key1', 436 | code_creation_date=old_enough) 437 | email2.save() 438 | 439 | user3 = User(username='user3', email='user3@example.com', is_active=False) 440 | user3.save() 441 | 442 | email3 = UserEmail(user=user3, email='user3@example.com', 443 | verified=False, default=True, 444 | verification_key='key2', 445 | code_creation_date=not_old_enough) 446 | email3.save() 447 | 448 | UserEmail.objects.delete_expired() 449 | 450 | user_ids = [user.id for user in User.objects.all()] 451 | user_email_ids = [user_email.id for user_email in UserEmail.objects.all()] 452 | 453 | self.assertEqual(list(sorted(user_ids)), list(sorted([user1.id, user3.id]))) 454 | self.assertEqual(list(sorted(user_email_ids)), list(sorted([email1.id, email3.id]))) 455 | -------------------------------------------------------------------------------- /emailauth/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | import emailauth.views 4 | 5 | urlpatterns = patterns('', 6 | url(r'^account/$', 'emailauth.views.account', name='emailauth_account'), 7 | 8 | url(r'^register/$', 'emailauth.views.register', 9 | name='register'), 10 | 11 | url(r'^register/continue/(?P.+)/$', 12 | 'emailauth.views.register_continue', 13 | name='emailauth_register_continue'), 14 | 15 | url(r'^verify/(?P\w+)/$', 'emailauth.views.verify', 16 | name='emailauth_verify'), 17 | 18 | url(r'^resetpassword/$', 'emailauth.views.request_password_reset', 19 | name='emailauth_request_password_reset'), 20 | url(r'^resetpassword/continue/(?P.+)/$', 21 | 'emailauth.views.request_password_reset_continue', 22 | name='emailauth_request_password_reset_continue'), 23 | url(r'^resetpassword/(?P\w+)/$', 24 | 'emailauth.views.reset_password', name='emailauth_reset_password'), 25 | 26 | url(r'^account/addemail/$', 'emailauth.views.add_email', 27 | name='emailauth_add_email'), 28 | url(r'^account/addemail/continue/(?P.+)/$', 29 | 'emailauth.views.add_email_continue', 30 | name='emailauth_add_email_continue'), 31 | url(r'^account/resendemail/(\d+)/$', 32 | 'emailauth.views.resend_verification_email', 33 | name='emailauth_resend_verification_email'), 34 | 35 | url(r'^account/changeemail/$', 'emailauth.views.change_email', 36 | name='emailauth_change_email'), 37 | url(r'^account/changeemail/continue/(?P.+)/$', 38 | 'emailauth.views.change_email_continue', 39 | name='emailauth_change_email_continue'), 40 | 41 | url(r'^account/deleteemail/(\d+)/$', 'emailauth.views.delete_email', 42 | name='emailauth_delete_email'), 43 | 44 | url(r'^account/setdefaultemail/(\d+)/$', 45 | 'emailauth.views.set_default_email', 46 | name='emailauth_set_default_email'), 47 | 48 | url(r'^login/$', 'emailauth.views.login', name='login'), 49 | url(r'^logout/$', 'django.contrib.auth.views.logout', {'next_page': '/', 50 | 'template_name': 'logged_out.html'}, name='logout'), 51 | ) 52 | -------------------------------------------------------------------------------- /emailauth/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import Http404 3 | from django.utils.functional import curry 4 | 5 | def email_verification_days(): 6 | return getattr(settings, 'EMAILAUTH_VERIFICATION_DAYS', 3) 7 | 8 | def use_single_email(): 9 | return getattr(settings, 'EMAILAUTH_USE_SINGLE_EMAIL', True) 10 | 11 | def use_automaintenance(): 12 | return getattr(settings, 'EMAILAUTH_USE_AUTOMAINTENANCE', True) 13 | 14 | def require_emailauth_mode(func, emailauth_use_singe_email): 15 | def wrapper(*args, **kwds): 16 | if use_single_email() == emailauth_use_singe_email: 17 | return func(*args, **kwds) 18 | else: 19 | raise Http404() 20 | return wrapper 21 | 22 | requires_single_email_mode = curry(require_emailauth_mode, 23 | emailauth_use_singe_email=True) 24 | 25 | requires_multi_emails_mode = curry(require_emailauth_mode, 26 | emailauth_use_singe_email=False) 27 | -------------------------------------------------------------------------------- /emailauth/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from urllib import urlencode, quote_plus 3 | 4 | import django.core.mail 5 | from django.conf import settings 6 | from django.contrib.auth import REDIRECT_FIELD_NAME 7 | from django.contrib.auth.models import User 8 | from django.contrib.sites.models import Site, RequestSite 9 | from django.core.urlresolvers import reverse 10 | from django.http import HttpResponseRedirect, Http404 11 | from django.shortcuts import render_to_response, get_object_or_404 12 | from django.template import RequestContext 13 | from django.template.loader import render_to_string 14 | from django.contrib.auth.decorators import login_required 15 | from django import forms 16 | import django.forms.forms 17 | import django.forms.util 18 | 19 | from django.utils.translation import ugettext_lazy as _ 20 | 21 | from emailauth.forms import (LoginForm, RegistrationForm, 22 | PasswordResetRequestForm, PasswordResetForm, AddEmailForm, DeleteEmailForm, 23 | ConfirmationForm) 24 | from emailauth.models import UserEmail 25 | 26 | from emailauth.utils import (use_single_email, requires_single_email_mode, 27 | requires_multi_emails_mode, email_verification_days) 28 | 29 | 30 | def login(request, template_name='emailauth/login.html', 31 | redirect_field_name=REDIRECT_FIELD_NAME): 32 | 33 | redirect_to = request.REQUEST.get(redirect_field_name, '') 34 | 35 | if request.method == 'POST': 36 | form = LoginForm(request.POST) 37 | if form.is_valid(): 38 | from django.contrib.auth import login 39 | login(request, form.get_user()) 40 | 41 | if request.get_host() == 'testserver': 42 | if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 43 | redirect_to = settings.LOGIN_REDIRECT_URL 44 | return HttpResponseRedirect(redirect_to) 45 | 46 | request.session.set_test_cookie() 47 | 48 | return HttpResponseRedirect(settings.LOGIN_URL + '?' + urlencode({ 49 | 'testcookiesupport': '', 50 | redirect_field_name: redirect_to, 51 | })) 52 | elif 'testcookiesupport' in request.GET: 53 | if request.session.test_cookie_worked(): 54 | request.session.delete_test_cookie() 55 | if not redirect_to or '//' in redirect_to or ' ' in redirect_to: 56 | redirect_to = settings.LOGIN_REDIRECT_URL 57 | return HttpResponseRedirect(redirect_to) 58 | else: 59 | form = LoginForm() 60 | errorlist = forms.util.ErrorList() 61 | errorlist.append(_("Your Web browser doesn't appear to " 62 | "have cookies enabled. Cookies are required for logging in.")) 63 | form._errors = forms.util.ErrorDict() 64 | form._errors[forms.forms.NON_FIELD_ERRORS] = errorlist 65 | else: 66 | form = LoginForm() 67 | 68 | if Site._meta.installed: 69 | current_site = Site.objects.get_current() 70 | else: 71 | current_site = RequestSite(request) 72 | 73 | return render_to_response(template_name, { 74 | 'form': form, 75 | redirect_field_name: redirect_to, 76 | 'site_name': current_site.name, 77 | }, 78 | context_instance=RequestContext(request)) 79 | 80 | 81 | @login_required 82 | def account(request, template_name=None): 83 | context = RequestContext(request) 84 | 85 | if template_name is None: 86 | if use_single_email(): 87 | template_name = 'emailauth/account_single_email.html' 88 | else: 89 | template_name = 'emailauth/account.html' 90 | 91 | # Maybe move this emails into context processors? 92 | extra_emails = UserEmail.objects.filter(user=request.user, default=False, 93 | verified=True) 94 | unverified_emails = UserEmail.objects.filter(user=request.user, 95 | default=False, verified=False) 96 | 97 | return render_to_response(template_name, 98 | { 99 | 'extra_emails': extra_emails, 100 | 'unverified_emails': unverified_emails, 101 | }, 102 | context_instance=context) 103 | 104 | 105 | def get_max_length(model, field_name): 106 | field = model._meta.get_field_by_name(field_name)[0] 107 | return field.max_length 108 | 109 | 110 | def default_register_callback(form, email): 111 | data = form.cleaned_data 112 | user = User() 113 | user.first_name = data['first_name'] 114 | user.is_active = False 115 | user.email = email.email 116 | user.set_password(data['password1']) 117 | user.save() 118 | user.username = ('id_%d_%s' % (user.id, user.email))[ 119 | :get_max_length(User, 'username')] 120 | user.save() 121 | email.user = user 122 | 123 | 124 | def register(request, callback=default_register_callback): 125 | if request.method == 'POST': 126 | form = RegistrationForm(request.POST) 127 | if form.is_valid(): 128 | email_obj = UserEmail.objects.create_unverified_email( 129 | form.cleaned_data['email']) 130 | email_obj.send_verification_email(form.cleaned_data['first_name']) 131 | 132 | if callback is not None: 133 | callback(form, email_obj) 134 | 135 | site = Site.objects.get_current() 136 | email_obj.user.message_set.create(message='Welcome to %s.' % site.name) 137 | 138 | email_obj.save() 139 | return HttpResponseRedirect(reverse('emailauth_register_continue', 140 | args=[quote_plus(email_obj.email)])) 141 | else: 142 | form = RegistrationForm() 143 | 144 | return render_to_response('emailauth/register.html', {'form': form}, 145 | RequestContext(request)) 146 | 147 | 148 | def register_continue(request, email, 149 | template_name='emailauth/register_continue.html'): 150 | 151 | return render_to_response(template_name, {'email': email}, 152 | RequestContext(request)) 153 | 154 | 155 | def default_verify_callback(request, email): 156 | email.user.is_active = True 157 | email.user.save() 158 | 159 | if request.user.is_anonymous(): 160 | from django.contrib.auth import login 161 | user = email.user 162 | user.backend = 'emailauth.backends.EmailBackend' 163 | login(request, user) 164 | return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) 165 | else: 166 | return HttpResponseRedirect(reverse('emailauth_account')) 167 | 168 | 169 | def verify(request, verification_key, template_name='emailauth/verify.html', 170 | extra_context=None, callback=default_verify_callback): 171 | 172 | verification_key = verification_key.lower() # Normalize before trying anything with it. 173 | email = UserEmail.objects.verify(verification_key) 174 | 175 | 176 | if email is not None: 177 | email.user.message_set.create(message=_('%s email confirmed.') % email.email) 178 | 179 | if use_single_email(): 180 | email.default = True 181 | email.save() 182 | UserEmail.objects.filter(user=email.user, default=False).delete() 183 | 184 | if email is not None and callback is not None: 185 | cb_result = callback(request, email) 186 | if cb_result is not None: 187 | return cb_result 188 | 189 | context = RequestContext(request) 190 | if extra_context is not None: 191 | for key, value in extra_context.items(): 192 | context[key] = value() if callable(value) else value 193 | 194 | return render_to_response(template_name, 195 | { 196 | 'email': email, 197 | 'expiration_days': email_verification_days(), 198 | }, 199 | context_instance=context) 200 | 201 | 202 | def request_password_reset(request, 203 | template_name='emailauth/request_password.html'): 204 | 205 | if request.method == 'POST': 206 | form = PasswordResetRequestForm(request.POST) 207 | if form.is_valid(): 208 | email = form.cleaned_data['email'] 209 | user_email = UserEmail.objects.get(email=email) 210 | user_email.make_new_key() 211 | user_email.save() 212 | 213 | current_site = Site.objects.get_current() 214 | 215 | subject = render_to_string( 216 | 'emailauth/request_password_email_subject.txt', 217 | {'site': current_site}) 218 | # Email subject *must not* contain newlines 219 | subject = ''.join(subject.splitlines()) 220 | 221 | message = render_to_string('emailauth/request_password_email.txt', { 222 | 'reset_code': user_email.verification_key, 223 | 'expiration_days': email_verification_days(), 224 | 'site': current_site, 225 | 'first_name': user_email.user.first_name, 226 | }) 227 | 228 | django.core.mail.send_mail(subject, message, 229 | settings.DEFAULT_FROM_EMAIL, [email]) 230 | 231 | return HttpResponseRedirect( 232 | reverse('emailauth_request_password_reset_continue', 233 | args=[quote_plus(email)])) 234 | else: 235 | form = PasswordResetRequestForm() 236 | 237 | context = RequestContext(request) 238 | return render_to_response(template_name, 239 | { 240 | 'form': form, 241 | 'expiration_days': email_verification_days(), 242 | }, 243 | context_instance=context) 244 | 245 | 246 | def request_password_reset_continue(request, email, 247 | template_name='emailauth/reset_password_continue.html'): 248 | 249 | return render_to_response(template_name, 250 | {'email': email}, 251 | context_instance=RequestContext(request)) 252 | 253 | 254 | def reset_password(request, reset_code, 255 | template_name='emailauth/reset_password.html'): 256 | 257 | user_email = get_object_or_404(UserEmail, verification_key=reset_code) 258 | if (user_email.verification_key == UserEmail.VERIFIED or 259 | user_email.code_creation_date + 260 | timedelta(days=email_verification_days()) < datetime.now()): 261 | 262 | raise Http404() 263 | 264 | if request.method == 'POST': 265 | form = PasswordResetForm(request.POST) 266 | if form.is_valid(): 267 | user = user_email.user 268 | user.set_password(form.cleaned_data['password1']) 269 | user.save() 270 | 271 | user_email.verification_key = UserEmail.VERIFIED 272 | user_email.save() 273 | 274 | from django.contrib.auth import login 275 | user.backend = 'emailauth.backends.EmailBackend' 276 | login(request, user) 277 | return HttpResponseRedirect(reverse('emailauth_account')) 278 | else: 279 | form = PasswordResetForm() 280 | 281 | context = RequestContext(request) 282 | return render_to_response(template_name, 283 | {'form': form}, 284 | context_instance=context) 285 | 286 | 287 | @requires_multi_emails_mode 288 | @login_required 289 | def add_email(request, template_name='emailauth/add_email.html'): 290 | if request.method == 'POST': 291 | form = AddEmailForm(request.POST) 292 | if form.is_valid(): 293 | email_obj = UserEmail.objects.create_unverified_email( 294 | form.cleaned_data['email'], user=request.user) 295 | email_obj.send_verification_email() 296 | email_obj.save() 297 | return HttpResponseRedirect(reverse('emailauth_add_email_continue', 298 | args=[quote_plus(email_obj.email)])) 299 | else: 300 | form = AddEmailForm() 301 | 302 | context = RequestContext(request) 303 | return render_to_response(template_name, 304 | {'form': form}, 305 | context_instance=context) 306 | 307 | 308 | @requires_multi_emails_mode 309 | @login_required 310 | def add_email_continue(request, email, 311 | template_name='emailauth/add_email_continue.html'): 312 | 313 | return render_to_response(template_name, 314 | {'email': email}, 315 | context_instance=RequestContext(request)) 316 | 317 | 318 | @requires_single_email_mode 319 | @login_required 320 | def change_email(request, template_name='emailauth/change_email.html'): 321 | if request.method == 'POST': 322 | form = AddEmailForm(request.POST) 323 | if form.is_valid(): 324 | UserEmail.objects.filter(user=request.user, default=False).delete() 325 | 326 | email_obj = UserEmail.objects.create_unverified_email( 327 | form.cleaned_data['email'], user=request.user) 328 | email_obj.send_verification_email() 329 | email_obj.save() 330 | 331 | return HttpResponseRedirect(reverse('emailauth_change_email_continue', 332 | args=[quote_plus(email_obj.email)])) 333 | else: 334 | form = AddEmailForm() 335 | 336 | context = RequestContext(request) 337 | return render_to_response(template_name, 338 | {'form': form}, 339 | context_instance=context) 340 | 341 | 342 | @requires_single_email_mode 343 | @login_required 344 | def change_email_continue(request, email, 345 | template_name='emailauth/change_email_continue.html'): 346 | 347 | return render_to_response(template_name, 348 | {'email': email}, 349 | context_instance=RequestContext(request)) 350 | 351 | 352 | @requires_multi_emails_mode 353 | @login_required 354 | def delete_email(request, email_id, 355 | template_name='emailauth/delete_email.html'): 356 | 357 | user_email = get_object_or_404(UserEmail, id=email_id, user=request.user, 358 | verified=True) 359 | 360 | if request.method == 'POST': 361 | form = DeleteEmailForm(request.user, request.POST) 362 | if form.is_valid(): 363 | user_email.delete() 364 | 365 | # Not really sure, where I should redirect from here... 366 | return HttpResponseRedirect(reverse('emailauth_account')) 367 | else: 368 | form = DeleteEmailForm(request.user) 369 | 370 | context = RequestContext(request) 371 | return render_to_response(template_name, 372 | {'form': form, 'email': user_email}, 373 | context_instance=context) 374 | 375 | 376 | @requires_multi_emails_mode 377 | @login_required 378 | def set_default_email(request, email_id, 379 | template_name='emailauth/set_default_email.html'): 380 | 381 | user_email = get_object_or_404(UserEmail, id=email_id, user=request.user, 382 | verified=True) 383 | 384 | if request.method == 'POST': 385 | form = ConfirmationForm(request.POST) 386 | if form.is_valid(): 387 | user_email.default = True 388 | user_email.save() 389 | return HttpResponseRedirect(reverse('emailauth_account')) 390 | else: 391 | form = ConfirmationForm() 392 | 393 | context = RequestContext(request) 394 | return render_to_response(template_name, 395 | {'form': form, 'email': user_email}, 396 | context_instance=context) 397 | 398 | 399 | @login_required 400 | def resend_verification_email(request, email_id): 401 | user_email = get_object_or_404(UserEmail, id=email_id, user=request.user, 402 | verified=False) 403 | user_email.send_verification_email() 404 | 405 | return HttpResponseRedirect(reverse('emailauth_add_email_continue', 406 | args=[quote_plus(user_email.email)])) 407 | 408 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/__init__.py -------------------------------------------------------------------------------- /example/emailauth.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/emailauth.db -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from os import path, environ 4 | from os.path import abspath, dirname, join 5 | import sys 6 | 7 | example_dir = dirname(abspath(__file__)) 8 | emailauth_dir = dirname(example_dir) 9 | 10 | sys.path.insert(0, example_dir) 11 | sys.path.insert(0, emailauth_dir) 12 | 13 | from django.core.management import execute_manager 14 | try: 15 | import settings # Assumed to be in the same directory. 16 | except ImportError: 17 | import sys 18 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 19 | sys.exit(1) 20 | 21 | if __name__ == "__main__": 22 | execute_manager(settings) 23 | -------------------------------------------------------------------------------- /example/media/css/clearfix.css: -------------------------------------------------------------------------------- 1 | .clearfix:after { 2 | content: "."; 3 | display: block; 4 | height: 0; 5 | clear: both; 6 | visibility: hidden; 7 | } 8 | 9 | .clearfix {display: inline-block;} 10 | 11 | /* Hides from IE-mac \*/ 12 | * html .clearfix {height: 1%;} 13 | .clearfix {display: block;} 14 | /* End hide from IE-mac */ 15 | -------------------------------------------------------------------------------- /example/media/css/fancy.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | fancy-type.css 4 | * Lots of pretty advanced classes for manipulating text. 5 | 6 | See the Readme file in this folder for additional instructions. 7 | 8 | -------------------------------------------------------------- */ 9 | 10 | /* Indentation instead of line shifts for sibling paragraphs. */ 11 | /*p + p { text-indent:2em; margin-top:-1.5em; }*/ 12 | /*form p + p { text-indent: 0; } [> Don't want this in forms. <]*/ 13 | 14 | 15 | /* For great looking type, use this code instead of asdf: 16 | asdf 17 | Best used on prepositions and ampersands. */ 18 | 19 | .alt { 20 | color: #666; 21 | font-family: "Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua", Georgia, serif; 22 | font-style: italic; 23 | font-weight: normal; 24 | } 25 | 26 | 27 | /* For great looking quote marks in titles, replace "asdf" with: 28 | asdf” 29 | (That is, when the title starts with a quote mark). 30 | (You may have to change this value depending on your font size). */ 31 | 32 | .dquo { margin-left: -.5em; } 33 | 34 | 35 | /* Reduced size type with incremental leading 36 | (http://www.markboulton.co.uk/journal/comments/incremental_leading/) 37 | 38 | This could be used for side notes. For smaller type, you don't necessarily want to 39 | follow the 1.5x vertical rhythm -- the line-height is too much. 40 | 41 | Using this class, it reduces your font size and line-height so that for 42 | every four lines of normal sized type, there is five lines of the sidenote. eg: 43 | 44 | New type size in em's: 45 | 10px (wanted side note size) / 12px (existing base size) = 0.8333 (new type size in ems) 46 | 47 | New line-height value: 48 | 12px x 1.5 = 18px (old line-height) 49 | 18px x 4 = 72px 50 | 72px / 5 = 14.4px (new line height) 51 | 14.4px / 10px = 1.44 (new line height in em's) */ 52 | 53 | p.incr, .incr p { 54 | font-size: 10px; 55 | line-height: 1.44em; 56 | margin-bottom: 1.5em; 57 | } 58 | 59 | 60 | /* Surround uppercase words and abbreviations with this class. 61 | Based on work by Jørgen Arnor Gårdsø Lom [http://twistedintellect.com/] */ 62 | 63 | .caps { 64 | font-variant: small-caps; 65 | letter-spacing: 1px; 66 | text-transform: lowercase; 67 | font-size:1.2em; 68 | line-height:1%; 69 | font-weight:bold; 70 | padding:0 2px; 71 | } 72 | -------------------------------------------------------------------------------- /example/media/css/forms.css: -------------------------------------------------------------------------------- 1 | .formfield { 2 | margin-top: 9px; 3 | } 4 | 5 | .fieldname { 6 | width: 200px; 7 | float: left; 8 | padding-top: 3px; 9 | padding-left: 20px; 10 | } 11 | 12 | .fieldnamefloat { 13 | float: left; 14 | padding-top: 3px; 15 | padding-left: 20px; 16 | padding-right: 10px; 17 | } 18 | 19 | .fieldnameflow { 20 | padding-top: 3px; 21 | padding-left: 20px; 22 | } 23 | 24 | .fieldname em { 25 | color: #bf1600; 26 | margin: 0px 0px 0px 5px; 27 | } 28 | 29 | .checkboxfield { 30 | float: left; 31 | padding-left: 20px; 32 | padding-top: 1px; 33 | width: 30px; 34 | } 35 | 36 | em.required { 37 | color: #bf1600; 38 | } 39 | 40 | .fieldvalue { 41 | float: left; 42 | } 43 | 44 | .fieldvalue.flow { 45 | padding-left: 20px; 46 | float: none; 47 | } 48 | 49 | .checkboxlabel { 50 | float: left; 51 | } 52 | 53 | .formfield .help { 54 | margin-top: 9px; 55 | width: 300px; 56 | } 57 | 58 | .checkboxlabel .help { 59 | margin-top: 9px; 60 | width: 470px; 61 | } 62 | 63 | .fieldvalue input { 64 | width: 150px; 65 | } 66 | 67 | .fieldvalue select { 68 | width: 150px; 69 | } 70 | 71 | .selectmultiple { 72 | height: 20em; 73 | } 74 | 75 | .fieldvalue input.long, .fieldvalue select.long, .fieldvalue textarea { 76 | width: 300px; 77 | } 78 | 79 | .fieldvalue input.short, .fieldvalue select.short { 80 | width: 75px; 81 | } 82 | 83 | .requiredfootnote { 84 | margin: 9px 0px; 85 | padding-left: 20px; 86 | } 87 | 88 | .submitrow { 89 | padding-left: 20px; 90 | } 91 | 92 | ul.errorlist { 93 | margin: 0px; 94 | list-style-type: none; 95 | padding: 2px 0px 2px 20px; 96 | color: #bf1600; 97 | background: url(/media/img/exclamation_white.png) no-repeat; 98 | } 99 | 100 | ul.errorlist li { 101 | font-size: 9pt; 102 | } 103 | 104 | .msg_error, .msg_info, .msg_question, .msg_success { 105 | background-position: 9px 7px; 106 | background-repeat: no-repeat; 107 | color: black; 108 | font-weight: bold; 109 | margin: 10px 0px 20px; 110 | padding: 6px 14px 6px 36px; 111 | vertical-align: middle; 112 | border: 2px solid #ddd; 113 | background-repeat: no-repeat; 114 | -moz-border-radius: 5px; 115 | border-radius: 5px; 116 | -webkit-border-radius: 5px; 117 | } 118 | 119 | 120 | .msg_error { 121 | background-color: #FBE3E4; 122 | color: #8a1f11; 123 | border-color: #FBC2C4; 124 | background-image: url(/media/img/error.png); 125 | } 126 | 127 | .msg_info { 128 | background-color: #E6EFC2; 129 | color: #264409; 130 | border-color: #C6D880; 131 | background-image: url(/media/img/info.png); 132 | } 133 | 134 | .msg_success { 135 | background-color: #E6EFC2; 136 | color: #264409; 137 | border-color: #C6D880; 138 | background-image: url(/media/img/success.png); 139 | } 140 | 141 | .msg_question { 142 | background-color:#D4F4CE; 143 | background-image: url(/media/img/question.png); 144 | border:1px solid #B9D6B4; 145 | } 146 | 147 | form table th { 148 | vertical-align: baseline; 149 | padding-top: 7px; 150 | } 151 | -------------------------------------------------------------------------------- /example/media/css/ie6.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/css/ie6.css -------------------------------------------------------------------------------- /example/media/css/reset.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | reset.css 4 | * Resets default browser CSS. 5 | 6 | -------------------------------------------------------------- */ 7 | 8 | html, body, div, span, object, iframe, 9 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 10 | a, abbr, acronym, address, code, 11 | del, dfn, em, img, q, dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-weight: inherit; 18 | font-style: inherit; 19 | font-size: 100%; 20 | font-family: inherit; 21 | vertical-align: baseline; 22 | } 23 | 24 | body { 25 | line-height: 1.5; 26 | } 27 | 28 | /* Tables still need 'cellspacing="0"' in the markup. */ 29 | table { border-collapse: separate; border-spacing: 0; } 30 | caption, th, td { text-align: left; font-weight: normal; } 31 | table, td, th { vertical-align: middle; } 32 | 33 | /* Remove possible quote marks (") from ,
. */ 34 | blockquote:before, blockquote:after, q:before, q:after { content: ""; } 35 | blockquote, q { quotes: "" ""; } 36 | 37 | /* Remove annoying border on linked images. */ 38 | a img { border: none; } 39 | -------------------------------------------------------------------------------- /example/media/css/site.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | } 8 | 9 | .wrapper { 10 | min-height: 100%; 11 | height: auto !important; 12 | height: 100%; 13 | margin: 0 auto -6em; 14 | } 15 | 16 | .footer, .push { 17 | height: 6em; 18 | } 19 | 20 | .header { 21 | background: #ddd; 22 | border-bottom: 2px solid #aaa; 23 | margin: 0px 0px 18px 0px; 24 | } 25 | 26 | .header h1 { 27 | font-size: 4em; 28 | float: left; 29 | } 30 | 31 | .header h1 a { 32 | text-decoration: none; 33 | } 34 | 35 | .logarea { 36 | float: right; 37 | line-height: 4em; 38 | } 39 | 40 | .logarea a, .logarea span { 41 | vertical-align: bottom; 42 | line-height: 1em; 43 | font-size: 14pt; 44 | } 45 | 46 | .footer { 47 | background: #ddd; 48 | text-align: center; 49 | } 50 | 51 | .innerfooter { 52 | border-top: 2px solid #aaa; 53 | padding: 9px 0px 0px 0px; 54 | } 55 | 56 | .content { 57 | padding-bottom: 36px; 58 | } 59 | 60 | .pagewidth { 61 | width: 960px; 62 | margin: 0px auto; 63 | } 64 | -------------------------------------------------------------------------------- /example/media/css/typography.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | typography.css 4 | * Sets up some sensible default typography. 5 | 6 | -------------------------------------------------------------- */ 7 | 8 | /* Default font settings. 9 | The font-size percentage is of 16px. (0.75 * 16px = 12px) */ 10 | body { 11 | font-size: 75%; 12 | color: #222; 13 | background: #fff; 14 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 15 | } 16 | 17 | 18 | /* Headings 19 | -------------------------------------------------------------- */ 20 | 21 | h1,h2,h3,h4,h5,h6 { font-weight: normal; color: #111; } 22 | 23 | h1 { font-size: 3em; line-height: 1; margin-bottom: 0.5em; } 24 | h2 { font-size: 2em; margin-bottom: 0.75em; } 25 | h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1em; } 26 | h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; } 27 | h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; } 28 | h6 { font-size: 1em; font-weight: bold; } 29 | 30 | h1 img, h2 img, h3 img, 31 | h4 img, h5 img, h6 img { 32 | margin: 0; 33 | } 34 | 35 | 36 | /* Text elements 37 | -------------------------------------------------------------- */ 38 | 39 | p { margin: 0 0 1.5em; } 40 | p img.left { float: left; margin: 1.5em 1.5em 1.5em 0; padding: 0; } 41 | p img.right { float: right; margin: 1.5em 0 1.5em 1.5em; } 42 | 43 | a:focus, 44 | a:hover { color: #000; } 45 | a { color: #009; text-decoration: underline; } 46 | 47 | blockquote { margin: 1.5em; color: #666; font-style: italic; } 48 | strong { font-weight: bold; } 49 | em,dfn { font-style: italic; } 50 | dfn { font-weight: bold; } 51 | sup, sub { line-height: 0; } 52 | 53 | abbr, 54 | acronym { border-bottom: 1px dotted #666; } 55 | address { margin: 0 0 1.5em; font-style: italic; } 56 | del { color:#666; } 57 | 58 | pre { margin: 1.5em 0; white-space: pre; } 59 | pre,code,tt { font: 1em 'andale mono', 'lucida console', monospace; line-height: 1.5; } 60 | 61 | 62 | /* Lists 63 | -------------------------------------------------------------- */ 64 | 65 | li ul, 66 | li ol { margin:0 1.5em; } 67 | ul, ol { margin: 0 1.5em 1.5em 1.5em; } 68 | 69 | ul { list-style-type: disc; } 70 | ol { list-style-type: decimal; } 71 | 72 | dl { margin: 0 0 1.5em 0; } 73 | dl dt { font-weight: bold; } 74 | dd { margin-left: 1.5em;} 75 | 76 | 77 | /* Tables 78 | -------------------------------------------------------------- */ 79 | 80 | table { margin-bottom: 1.4em; } 81 | th { font-weight: bold; } 82 | thead th { background: #c3d9ff; } 83 | th,td,caption { padding: 4px 10px 4px 5px; } 84 | tr.even td { background: #e5ecf9; } 85 | tfoot { font-style: italic; } 86 | caption { background: #eee; } 87 | 88 | 89 | /* Misc classes 90 | -------------------------------------------------------------- */ 91 | 92 | .small { font-size: .8em; margin-bottom: 1.875em; line-height: 1.875em; } 93 | .large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; } 94 | .hide { display: none; } 95 | 96 | .quiet { color: #666; } 97 | .loud { color: #000; } 98 | .highlight { background:#ff0; } 99 | .added { background:#060; color: #fff; } 100 | .removed { background:#900; color: #fff; } 101 | 102 | .first { margin-left:0; padding-left:0; } 103 | .last { margin-right:0; padding-right:0; } 104 | .top { margin-top:0; padding-top:0; } 105 | .bottom { margin-bottom:0; padding-bottom:0; } 106 | -------------------------------------------------------------------------------- /example/media/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/img/error.png -------------------------------------------------------------------------------- /example/media/img/exclamation_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/img/exclamation_white.png -------------------------------------------------------------------------------- /example/media/img/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/img/info.png -------------------------------------------------------------------------------- /example/media/img/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/img/question.png -------------------------------------------------------------------------------- /example/media/img/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redvasily/django-emailauth/1c3c977f361e63eb6e4bd2fa32f6d8af78f74f31/example/media/img/success.png -------------------------------------------------------------------------------- /example/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.models import Site 3 | 4 | class CurrentSiteMiddleware(object): 5 | def process_request(self, request): 6 | site = Site.objects.get(id=settings.SITE_ID) 7 | if site.domain != request.get_host(): 8 | site.domain = request.get_host() 9 | site.save() 10 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | from os.path import join, dirname, abspath 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | PROJECT_ROOT = dirname(abspath(__file__)) 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@domain.com'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | DATABASE_ENGINE = 'sqlite3' 16 | 17 | # Or path to database file if using sqlite3. 18 | DATABASE_NAME = join(PROJECT_ROOT, 'emailauth.db') 19 | 20 | # Not used with sqlite3. 21 | DATABASE_USER = '' 22 | 23 | # Not used with sqlite3. 24 | DATABASE_PASSWORD = '' 25 | 26 | # Set to empty string for localhost. Not used with sqlite3. 27 | DATABASE_HOST = '' 28 | 29 | # Set to empty string for default. Not used with sqlite3. 30 | DATABASE_PORT = '' 31 | 32 | # Local time zone for this installation. Choices can be found here: 33 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 34 | # although not all choices may be available on all operating systems. 35 | # If running in a Windows environment this must be set to the same as your 36 | # system time zone. 37 | TIME_ZONE = 'America/Chicago' 38 | 39 | # Language code for this installation. All choices can be found here: 40 | # http://www.i18nguy.com/unicode/language-identifiers.html 41 | LANGUAGE_CODE = 'en-us' 42 | 43 | SITE_ID = 1 44 | 45 | # If you set this to False, Django will make some optimizations so as not 46 | # to load the internationalization machinery. 47 | USE_I18N = True 48 | 49 | # Absolute path to the directory that holds media. 50 | # Example: "/home/media/media.lawrence.com/" 51 | MEDIA_ROOT = join(PROJECT_ROOT, 'media') 52 | 53 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 54 | # trailing slash if there is a path component (optional in other cases). 55 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 56 | MEDIA_URL = '/media/' 57 | 58 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 59 | # trailing slash. 60 | # Examples: "http://foo.com/media/", "/media/". 61 | ADMIN_MEDIA_PREFIX = '/media/admin/' 62 | 63 | # Make this unique, and don't share it with anybody. 64 | SECRET_KEY = '-&umm_!rboq^$-ye%v+4rp^+@a&dqou&d=%psw(xvfh)y%p2q-' 65 | 66 | # List of callables that know how to import templates from various sources. 67 | TEMPLATE_LOADERS = ( 68 | 'django.template.loaders.filesystem.load_template_source', 69 | 'django.template.loaders.app_directories.load_template_source', 70 | # 'django.template.loaders.eggs.load_template_source', 71 | ) 72 | 73 | MIDDLEWARE_CLASSES = ( 74 | 'django.middleware.common.CommonMiddleware', 75 | 'django.contrib.sessions.middleware.SessionMiddleware', 76 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 77 | 'example.middleware.CurrentSiteMiddleware', 78 | ) 79 | 80 | ROOT_URLCONF = 'example.urls' 81 | 82 | TEMPLATE_DIRS = ( 83 | join(PROJECT_ROOT, 'templates'), 84 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 85 | # Always use forward slashes, even on Windows. 86 | # Don't forget to use absolute paths, not relative paths. 87 | ) 88 | 89 | INSTALLED_APPS = ( 90 | 'django.contrib.auth', 91 | 'django.contrib.contenttypes', 92 | 'django.contrib.sessions', 93 | 'django.contrib.sites', 94 | 'django.contrib.admin', 95 | 'emailauth', 96 | ) 97 | 98 | AUTHENTICATION_BACKENDS = ( 99 | 'emailauth.backends.EmailBackend', 100 | 'emailauth.backends.FallbackBackend', 101 | ) 102 | 103 | LOGIN_REDIRECT_URL = '/account/' 104 | LOGIN_URL = '/login/' 105 | 106 | EMAILAUTH_USE_SINGLE_EMAIL = False 107 | 108 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 109 | -------------------------------------------------------------------------------- /example/settings_singleemail.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | EMAILAUTH_USE_SINGLE_EMAIL = True 4 | -------------------------------------------------------------------------------- /example/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Emailauth example project 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

404

13 | 14 | 15 | 16 | {# vim: set ft=htmldjango: #} 17 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block content %} 4 | {{ content|safe }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /example/templates/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Emailauth example project 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |

Emailauth example project

24 | {% block logarea %} 25 |
26 | {% if user.is_authenticated %} 27 | Welcome, {{ user.first_name }}. 28 | Account 29 | Logout 30 | {% else %} 31 | Register | Login 32 | {% endif %} 33 |
34 | {% endblock %} 35 |
36 |
37 |
38 |
39 |

{% block title %}{{ title }}{% endblock %}

40 |
41 | {% if messages %} 42 | {% for message in messages %} 43 |
{{ message }}
44 | {% endfor %} 45 | {% endif %} 46 | 47 | {% block content %} 48 | {% endblock %} 49 |
50 |
51 |
52 |
53 | 62 | 63 | {#{% block logarea %}#} 64 | {# {% if user.is_authenticated %}#} 65 | {# Welcome, {{ user.first_name }}.
#} 66 | {# Username: {{ user.username }}
#} 67 | {# Email: {{ user.email }}
#} 68 | {# Logout#} 69 | {# {% else %}#} 70 | {# Register | Login#} 71 | {# {% endif %}#} 72 | {#{% endblock %}#} 73 | 74 | {#{% block content %}#} 75 | {#{% endblock %}#} 76 | 77 | 78 | 79 | {# vim: set ft=htmldjango: #} 80 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.conf import settings 3 | from django.views.generic.simple import direct_to_template 4 | 5 | from django.contrib import admin 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | url(r'^$', 'example.views.index', name='index'), 10 | (r'', include('emailauth.urls')), 11 | (r'^admin/(.*)', admin.site.root), 12 | ) 13 | 14 | urlpatterns += patterns('', 15 | (r'^media/(?P.*)$', 'django.views.static.serve', 16 | { 17 | 'document_root': settings.MEDIA_ROOT, 18 | 'show_indexes': True 19 | } 20 | ), 21 | ) 22 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, abspath, join 2 | 3 | from django.shortcuts import render_to_response 4 | from django import template 5 | from django.template import RequestContext 6 | from django.contrib.markup.templatetags.markup import restructuredtext 7 | 8 | def index(request): 9 | readme_file = join(dirname(dirname(abspath(__file__))), 'README.rst') 10 | raw_content = open(readme_file).read() 11 | try: 12 | content = restructuredtext(raw_content) 13 | except template.TemplateSyntaxError: 14 | content = u'
' + raw_content + u'
' 15 | 16 | return render_to_response('index.html', {'content': content}, 17 | context_instance=RequestContext(request)) 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import os 3 | 4 | # Compile the list of packages available, because distutils doesn't have 5 | # an easy way to do this. 6 | packages, data_files = [], [] 7 | root_dir = os.path.dirname(__file__) 8 | if root_dir: 9 | os.chdir(root_dir) 10 | 11 | for dirpath, dirnames, filenames in os.walk('emailauth'): 12 | # Ignore dirnames that start with '.' 13 | for i, dirname in enumerate(dirnames): 14 | if dirname.startswith('.'): del dirnames[i] 15 | if '__init__.py' in filenames: 16 | pkg = dirpath.replace(os.path.sep, '.') 17 | if os.path.altsep: 18 | pkg = pkg.replace(os.path.altsep, '.') 19 | packages.append(pkg) 20 | elif filenames: 21 | prefix = dirpath[len('emailauth/'):] # Strip package prefix 22 | for f in filenames: 23 | data_files.append(os.path.join(prefix, f)) 24 | 25 | setup(name='django-emailauth', 26 | version='0.1', 27 | description='User email authentication application for Django', 28 | author='Vasily Sulatskov', 29 | author_email='redvasily@gmail.com', 30 | url='http://github.com/redvasily/django-emailauth/tree/master/', 31 | download_url='http://cloud.github.com/downloads/redvasily/django-emailauth/django-emailauth-0.1.tar.gz', 32 | package_dir={ 33 | 'emailauth': 'emailauth', 34 | }, 35 | packages=packages, 36 | package_data={ 37 | 'emailauth': data_files 38 | }, 39 | classifiers=[ 40 | 'Development Status :: 4 - Beta', 41 | 'Environment :: Web Environment', 42 | 'Framework :: Django', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: BSD License', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python', 47 | 'Topic :: Utilities', 48 | ], 49 | ) 50 | --------------------------------------------------------------------------------