diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..222ad582 --- /dev/null +++ b/.tx/config @@ -0,0 +1,7 @@ +[main] +host = https://www.transifex.net + +[django-user-accounts.djangopo] +file_filter = account/locale//LC_MESSAGES/django.po +source_lang = en +source_file = account/locale/en/LC_MESSAGES/django.po diff --git a/README.rst b/README.rst index 1ddd0a6e..d1293cef 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,7 @@ Requirements * Django 1.4 * django-appconf (included in ``install_requires``) +* pytz (included in ``install_requires``) Documentation ============= diff --git a/account/admin.py b/account/admin.py new file mode 100644 index 00000000..169e62b2 --- /dev/null +++ b/account/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from account.models import SignupCode + + +class SignupCodeAdmin(admin.ModelAdmin): + list_display = ["code", "max_uses", "use_count", "expiry", "created"] + search_fields = ["code", "email"] + list_filter = ["created"] + + +admin.site.register(SignupCode, SignupCodeAdmin) \ No newline at end of file diff --git a/account/conf.py b/account/conf.py index 8f8e7c14..633806b4 100644 --- a/account/conf.py +++ b/account/conf.py @@ -15,15 +15,17 @@ class AccountAppConf(AppConf): LOGOUT_REDIRECT_URL = "/" PASSWORD_CHANGE_REDIRECT_URL = "account_password" PASSWORD_RESET_REDIRECT_URL = "account_login" + REMEMBER_ME_EXPIRY = 60*60*24*365*10 USER_DISPLAY = lambda user: user.username EMAIL_UNIQUE = True EMAIL_CONFIRMATION_REQUIRED = False + EMAIL_CONFIRMATION_EMAIL = True EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "account_login" EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = None SETTINGS_REDIRECT_URL = "account_settings" CONTACT_EMAIL = "support@example.com" - TIMEZONE_CHOICES = list(zip(pytz.all_timezones, pytz.all_timezones)) + TIMEZONES = zip(pytz.all_timezones, pytz.all_timezones) LANGUAGES = [ (code, get_language_info(code).get("name_local")) for code, lang in settings.LANGUAGES diff --git a/account/fields.py b/account/fields.py index 114354d5..63363a72 100644 --- a/account/fields.py +++ b/account/fields.py @@ -11,7 +11,7 @@ def __init__(self, *args, **kwargs): defaults = { "max_length": 100, "default": settings.TIME_ZONE, - "choices": settings.ACCOUNT_TIMEZONE_CHOICES + "choices": settings.ACCOUNT_TIMEZONES } defaults.update(kwargs) return super(TimeZoneField, self).__init__(*args, **defaults) diff --git a/account/forms.py b/account/forms.py index ccedb2dc..f512eb3c 100644 --- a/account/forms.py +++ b/account/forms.py @@ -21,11 +21,11 @@ class SignupForm(forms.Form): widget=forms.TextInput(), required=True ) - password1 = forms.CharField( + password = forms.CharField( label=_("Password"), widget=forms.PasswordInput(render_value=False) ) - password2 = forms.CharField( + password_confirm = forms.CharField( label=_("Password (again)"), widget=forms.PasswordInput(render_value=False) ) @@ -52,8 +52,8 @@ def clean_email(self): raise forms.ValidationError(_("A user is registered with this email address.")) def clean(self): - if "password1" in self.cleaned_data and "password2" in self.cleaned_data: - if self.cleaned_data["password1"] != self.cleaned_data["password2"]: + if "password" in self.cleaned_data and "password_confirm" in self.cleaned_data: + if self.cleaned_data["password"] != self.cleaned_data["password_confirm"]: raise forms.ValidationError(_("You must type the same password each time.")) return self.cleaned_data @@ -121,15 +121,15 @@ def user_credentials(self): class ChangePasswordForm(forms.Form): - oldpassword = forms.CharField( + password_current = forms.CharField( label=_("Current Password"), widget=forms.PasswordInput(render_value=False) ) - password1 = forms.CharField( + password_new = forms.CharField( label=_("New Password"), widget=forms.PasswordInput(render_value=False) ) - password2 = forms.CharField( + password_new_confirm = forms.CharField( label=_("New Password (again)"), widget=forms.PasswordInput(render_value=False) ) @@ -138,19 +138,19 @@ def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") super(ChangePasswordForm, self).__init__(*args, **kwargs) - def clean_oldpassword(self): - if not self.user.check_password(self.cleaned_data.get("oldpassword")): + def clean_password_current(self): + if not self.user.check_password(self.cleaned_data.get("password_current")): raise forms.ValidationError(_("Please type your current password.")) - return self.cleaned_data["oldpassword"] + return self.cleaned_data["password_current"] - def clean_password2(self): - if "password1" in self.cleaned_data and "password2" in self.cleaned_data: - if self.cleaned_data["password1"] != self.cleaned_data["password2"]: + def clean_password_new_confirm(self): + if "password_new" in self.cleaned_data and "password_new_confirm" in self.cleaned_data: + if self.cleaned_data["password_new"] != self.cleaned_data["password_new_confirm"]: raise forms.ValidationError(_("You must type the same password each time.")) - return self.cleaned_data["password2"] + return self.cleaned_data["password_new_confirm"] def save(self, user): - user.set_password(self.cleaned_data["password1"]) + user.set_password(self.cleaned_data["password_new"]) user.save() @@ -170,20 +170,20 @@ def clean_email(self): class PasswordResetTokenForm(forms.Form): - password1 = forms.CharField( + password = forms.CharField( label = _("New Password"), widget = forms.PasswordInput(render_value=False) ) - password2 = forms.CharField( + password_confirm = forms.CharField( label = _("New Password (again)"), widget = forms.PasswordInput(render_value=False) ) - def clean_password2(self): - if "password1" in self.cleaned_data and "password2" in self.cleaned_data: - if self.cleaned_data["password1"] != self.cleaned_data["password2"]: + def clean_password_confirm(self): + if "password" in self.cleaned_data and "password_confirm" in self.cleaned_data: + if self.cleaned_data["password"] != self.cleaned_data["password_confirm"]: raise forms.ValidationError(_("You must type the same password each time.")) - return self.cleaned_data["password2"] + return self.cleaned_data["password_confirm"] class SettingsForm(forms.Form): @@ -191,17 +191,20 @@ class SettingsForm(forms.Form): email = forms.EmailField(label=_("Email"), required=True) timezone = forms.ChoiceField( label=_("Timezone"), - choices=settings.ACCOUNT_TIMEZONE_CHOICES, - required=False - ) - language = forms.ChoiceField( - label=_("Language"), - choices=settings.ACCOUNT_LANGUAGES, + choices=settings.ACCOUNT_TIMEZONES, required=False ) + if settings.USE_I18N: + language = forms.ChoiceField( + label=_("Language"), + choices=settings.ACCOUNT_LANGUAGES, + required=False + ) def clean_email(self): value = self.cleaned_data["email"] + if self.initial.get("email") == value: + return value qs = EmailAddress.objects.filter(email__iexact=value) if not qs.exists() or not settings.ACCOUNT_EMAIL_UNIQUE: return value diff --git a/account/locale/en/LC_MESSAGES/django.mo b/account/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 00000000..1b3e8ef8 Binary files /dev/null and b/account/locale/en/LC_MESSAGES/django.mo differ diff --git a/account/locale/en/LC_MESSAGES/django.po b/account/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..d4bf4f12 --- /dev/null +++ b/account/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-05-15 13:38-0600\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:19 forms.py:92 +msgid "Username" +msgstr "" + +#: forms.py:25 forms.py:64 +msgid "Password" +msgstr "" + +#: forms.py:29 +msgid "Password (again)" +msgstr "" + +#: forms.py:41 +msgid "Usernames can only contain letters, numbers and underscores." +msgstr "" + +#: forms.py:45 +msgid "This username is already taken. Please choose another." +msgstr "" + +#: forms.py:52 forms.py:211 +msgid "A user is registered with this email address." +msgstr "" + +#: forms.py:57 forms.py:149 forms.py:185 +msgid "You must type the same password each time." +msgstr "" + +#: forms.py:68 +msgid "Remember Me" +msgstr "" + +#: forms.py:81 +msgid "This account is inactive." +msgstr "" + +#: forms.py:93 +msgid "The username and/or password you specified are not correct." +msgstr "" + +#: forms.py:108 forms.py:159 forms.py:191 +msgid "Email" +msgstr "" + +#: forms.py:109 +msgid "The email address and/or password you specified are not correct." +msgstr "" + +#: forms.py:125 +msgid "Current Password" +msgstr "" + +#: forms.py:129 forms.py:174 +msgid "New Password" +msgstr "" + +#: forms.py:133 forms.py:178 +msgid "New Password (again)" +msgstr "" + +#: forms.py:143 +msgid "Please type your current password." +msgstr "" + +#: forms.py:164 +msgid "Email address not verified for any user account" +msgstr "" + +#: forms.py:167 +msgid "Email address not found for any user account" +msgstr "" + +#: forms.py:193 +msgid "Timezone" +msgstr "" + +#: forms.py:199 +msgid "Language" +msgstr "" + +#: models.py:29 +msgid "user" +msgstr "" + +#: models.py:30 +msgid "timezone" +msgstr "" + +#: models.py:31 +msgid "language" +msgstr "" + +#: models.py:209 +msgid "email address" +msgstr "" + +#: models.py:210 +msgid "email addresses" +msgstr "" + +#: models.py:246 +msgid "email confirmation" +msgstr "" + +#: models.py:247 +msgid "email confirmations" +msgstr "" + +#: views.py:36 +#, python-format +msgid "Confirmation email sent to %(email)s." +msgstr "" + +#: views.py:40 +#, python-format +msgid "Successfully logged in as %(user)s." +msgstr "" + +#: views.py:44 +#, python-format +msgid "The code %(code)s is invalid." +msgstr "" + +#: views.py:267 +#, python-format +msgid "You have confirmed %(email)s." +msgstr "" + +#: views.py:340 views.py:441 +msgid "Password successfully changed." +msgstr "" + +#: views.py:502 +msgid "Account settings updated." +msgstr "" diff --git a/account/locale/es/LC_MESSAGES/django.mo b/account/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 00000000..f0ef3f3b Binary files /dev/null and b/account/locale/es/LC_MESSAGES/django.mo differ diff --git a/account/locale/es/LC_MESSAGES/django.po b/account/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..fbb57faf --- /dev/null +++ b/account/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,155 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Erik Rivera , 2012. +msgid "" +msgstr "" +"Project-Id-Version: django-user-accounts\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-05-15 13:38-0600\n" +"PO-Revision-Date: 2012-05-15 18:11+0000\n" +"Last-Translator: Erik Rivera \n" +"Language-Team: Spanish (Castilian) (http://www.transifex.net/projects/p/pinax/language/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: forms.py:19 forms.py:92 +msgid "Username" +msgstr "Nombre de usuario" + +#: forms.py:25 forms.py:64 +msgid "Password" +msgstr "Contraseña" + +#: forms.py:29 +msgid "Password (again)" +msgstr "Contraseña (repetir)" + +#: forms.py:41 +msgid "Usernames can only contain letters, numbers and underscores." +msgstr "Los nombres de usuario solo pueden contener letras, números y subguiones" + +#: forms.py:45 +msgid "This username is already taken. Please choose another." +msgstr "Este nombre de usuario ya está en uso. Por favor elija otro." + +#: forms.py:52 forms.py:211 +msgid "A user is registered with this email address." +msgstr "Un usuario se ha registrado con esta dirección de correo electrónico." + +#: forms.py:57 forms.py:149 forms.py:185 +msgid "You must type the same password each time." +msgstr "Debe escribir la misma contraseña cada vez." + +#: forms.py:68 +msgid "Remember Me" +msgstr "Recordarme" + +#: forms.py:81 +msgid "This account is inactive." +msgstr "Esta cuenta está inactiva." + +#: forms.py:93 +msgid "The username and/or password you specified are not correct." +msgstr "El nombre de usuario y/o la contraseña que ha especificado no son correctas." + +#: forms.py:108 forms.py:159 forms.py:191 +msgid "Email" +msgstr "Correo electrónico" + +#: forms.py:109 +msgid "The email address and/or password you specified are not correct." +msgstr "La dirección de correo electrónico y/o la contraseña que ha especificado no son correctas." + +#: forms.py:125 +msgid "Current Password" +msgstr "Contraseña actual" + +#: forms.py:129 forms.py:174 +msgid "New Password" +msgstr "Contraseña nueva" + +#: forms.py:133 forms.py:178 +msgid "New Password (again)" +msgstr "Contraseña nueva (repetir)" + +#: forms.py:143 +msgid "Please type your current password." +msgstr "Por favor escriba su contraseña actual." + +#: forms.py:164 +msgid "Email address not verified for any user account" +msgstr "Correo electrónico no verificado para ninguna cuenta de usuario." + +#: forms.py:167 +msgid "Email address not found for any user account" +msgstr "Correo electrónico no encontrado para ninguna cuenta de usuario." + +#: forms.py:193 +msgid "Timezone" +msgstr "Zona horaria" + +#: forms.py:199 +msgid "Language" +msgstr "Idioma" + +#: models.py:29 +msgid "user" +msgstr "usuario" + +#: models.py:30 +msgid "timezone" +msgstr "zona horaria" + +#: models.py:31 +msgid "language" +msgstr "idioma" + +#: models.py:209 +msgid "email address" +msgstr "correo electrónico" + +#: models.py:210 +msgid "email addresses" +msgstr "correos electrónicos" + +#: models.py:246 +msgid "email confirmation" +msgstr "confirmación de correo electrónico" + +#: models.py:247 +msgid "email confirmations" +msgstr "confirmaciones de correos electrónicos" + +#: views.py:36 +#, python-format +msgid "Confirmation email sent to %(email)s." +msgstr "La confirmación de correo electrónico se ha enviado a %(email)s." + +#: views.py:40 +#, python-format +msgid "Successfully logged in as %(user)s." +msgstr "Inicio la sesión satisfactoriamente como %(user)s." + +#: views.py:44 +#, python-format +msgid "The code %(code)s is invalid." +msgstr "El código %(code)s no es válido." + +#: views.py:267 +#, python-format +msgid "You have confirmed %(email)s." +msgstr "Has confirmado %(email)s." + +#: views.py:340 views.py:441 +msgid "Password successfully changed." +msgstr "La contraseña se ha cambiado con éxito." + +#: views.py:502 +msgid "Account settings updated." +msgstr "Los ajustes de la cuenta actualizados." diff --git a/account/locale/it/LC_MESSAGES/django.mo b/account/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 00000000..f214b38c Binary files /dev/null and b/account/locale/it/LC_MESSAGES/django.mo differ diff --git a/account/locale/it/LC_MESSAGES/django.po b/account/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..ae6eb51d --- /dev/null +++ b/account/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: django-user-accounts\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2012-05-15 13:38-0600\n" +"PO-Revision-Date: 2012-05-15 19:23+0000\n" +"Last-Translator: Massimiliano Ravelli \n" +"Language-Team: Italian (http://www.transifex.net/projects/p/pinax/language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: forms.py:19 forms.py:92 +msgid "Username" +msgstr "Nome utente" + +#: forms.py:25 forms.py:64 +msgid "Password" +msgstr "Password" + +#: forms.py:29 +msgid "Password (again)" +msgstr "Password (di nuovo)" + +#: forms.py:41 +msgid "Usernames can only contain letters, numbers and underscores." +msgstr "Il nome utente può contenere solo lettere, numeri e trattini bassi." + +#: forms.py:45 +msgid "This username is already taken. Please choose another." +msgstr "Questo nome utente è già utilizzato. Scegline un'altro." + +#: forms.py:52 forms.py:211 +msgid "A user is registered with this email address." +msgstr "Esiste già un utente con questo indirizzo email." + +#: forms.py:57 forms.py:149 forms.py:185 +msgid "You must type the same password each time." +msgstr "La password deve essere inserita due volte." + +#: forms.py:68 +msgid "Remember Me" +msgstr "Ricordami" + +#: forms.py:81 +msgid "This account is inactive." +msgstr "Questo account non è attivo." + +#: forms.py:93 +msgid "The username and/or password you specified are not correct." +msgstr "Il nome utente e/o la password non sono corretti." + +#: forms.py:108 forms.py:159 forms.py:191 +msgid "Email" +msgstr "Email" + +#: forms.py:109 +msgid "The email address and/or password you specified are not correct." +msgstr "L'indirizzo email e/o la password non sono corretti." + +#: forms.py:125 +msgid "Current Password" +msgstr "Password Attuale" + +#: forms.py:129 forms.py:174 +msgid "New Password" +msgstr "Nuova Password" + +#: forms.py:133 forms.py:178 +msgid "New Password (again)" +msgstr "Nuova Password (di nuovo)" + +#: forms.py:143 +msgid "Please type your current password." +msgstr "Inserisci la password attuale." + +#: forms.py:164 +msgid "Email address not verified for any user account" +msgstr "Indirizzo email non verificato" + +#: forms.py:167 +msgid "Email address not found for any user account" +msgstr "Indirizzo email non trovato" + +#: forms.py:193 +msgid "Timezone" +msgstr "Fuso orario" + +#: forms.py:199 +msgid "Language" +msgstr "Lingua" + +#: models.py:29 +msgid "user" +msgstr "utente" + +#: models.py:30 +msgid "timezone" +msgstr "fuso orario" + +#: models.py:31 +msgid "language" +msgstr "lingua" + +#: models.py:209 +msgid "email address" +msgstr "indirizzo email" + +#: models.py:210 +msgid "email addresses" +msgstr "indirizzi email" + +#: models.py:246 +msgid "email confirmation" +msgstr "conferma indirizzo email" + +#: models.py:247 +msgid "email confirmations" +msgstr "conferme indirizzi email" + +#: views.py:36 +#, python-format +msgid "Confirmation email sent to %(email)s." +msgstr "Email di conferma inviata a %(email)s." + +#: views.py:40 +#, python-format +msgid "Successfully logged in as %(user)s." +msgstr "Accesso effettuato come %(user)s." + +#: views.py:44 +#, python-format +msgid "The code %(code)s is invalid." +msgstr "Il codice %(code)s non è valido." + +#: views.py:267 +#, python-format +msgid "You have confirmed %(email)s." +msgstr "Hai confermato %(email)s." + +#: views.py:340 views.py:441 +msgid "Password successfully changed." +msgstr "Password cambiata con successo." + +#: views.py:502 +msgid "Account settings updated." +msgstr "Configurazione account aggiornata." diff --git a/account/managers.py b/account/managers.py index cf97a893..4695f160 100644 --- a/account/managers.py +++ b/account/managers.py @@ -1,5 +1,7 @@ from django.db import models, IntegrityError +from account.conf import settings + class EmailAddressManager(models.Manager): @@ -9,7 +11,8 @@ def add_email(self, user, email, **kwargs): except IntegrityError: return None else: - email_address.send_confirmation() + if settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL: + email_address.send_confirmation() return email_address def get_primary(self, user): diff --git a/middleware.py b/account/middleware.py similarity index 100% rename from middleware.py rename to account/middleware.py diff --git a/account/models.py b/account/models.py index 1b3e4ba6..cbd3d166 100644 --- a/account/models.py +++ b/account/models.py @@ -7,14 +7,15 @@ from django.db import models from django.db.models import Q from django.db.models.signals import post_save -from django.dispatch import receiver from django.template.loader import render_to_string -from django.utils import timezone -from django.utils.translation import get_language_from_request, gettext_lazy as _ +from django.utils import timezone, translation +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User, AnonymousUser from django.contrib.sites.models import Site +import pytz + from account import signals from account.conf import settings from account.fields import TimeZoneField @@ -26,12 +27,11 @@ class Account(models.Model): user = models.OneToOneField(User, related_name="account", verbose_name=_("user")) - timezone = TimeZoneField(_("timezone")) language = models.CharField(_("language"), - max_length = 10, - choices = settings.ACCOUNT_LANGUAGES, - default = settings.LANGUAGE_CODE + max_length=10, + choices=settings.ACCOUNT_LANGUAGES, + default=settings.LANGUAGE_CODE ) @classmethod @@ -45,8 +45,27 @@ def for_request(cls, request): account = AnonymousAccount(request) return account + @classmethod + def create(cls, request=None, **kwargs): + account = cls(**kwargs) + if "language" not in kwargs: + if request is None: + account.language = settings.LANGUAGE_CODE + else: + account.language = translation.get_language_from_request(request, check_path=True) + account.save() + return account + def __unicode__(self): return self.user.username + + def now(self): + """ + Returns a timezone aware datetime localized to the account's timezone. + """ + naive = datetime.datetime.now() + aware = naive.replace(tzinfo=pytz.timezone(settings.TIME_ZONE)) + return aware.astimezone(pytz.timezone(self.timezone)) class AnonymousAccount(object): @@ -54,10 +73,10 @@ class AnonymousAccount(object): def __init__(self, request=None): self.user = AnonymousUser() self.timezone = settings.TIME_ZONE - if request is not None: - self.language = get_language_from_request(request) - else: + if request is None: self.language = settings.LANGUAGE_CODE + else: + self.language = translation.get_language_from_request(request, check_path=True) def __unicode__(self): return "AnonymousAccount" @@ -144,9 +163,9 @@ def use(self, user): result.save() signup_code_used.send(sender=result.__class__, signup_code_result=result) - def send(self): + def send(self, **kwargs): protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") - current_site = Site.objects.get_current() + current_site = kwargs["site"] if "site" in kwargs else Site.objects.get_current() signup_url = u"%s://%s%s?%s" % ( protocol, unicode(current_site.domain), @@ -249,8 +268,8 @@ def confirm(self): signals.email_confirmed.send(sender=self.__class__, email_address=email_address) return email_address - def send(self): - current_site = Site.objects.get_current() + def send(self, **kwargs): + current_site = kwargs["site"] if "site" in kwargs else Site.objects.get_current() protocol = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http") activate_url = u"%s://%s%s" % ( protocol, @@ -270,9 +289,3 @@ def send(self): self.sent = timezone.now() self.save() signals.email_confirmation_sent.send(sender=self.__class__, confirmation=self) - - -@receiver(post_save, sender=User) -def create_account(sender, **kwargs): - if kwargs["created"]: - Account.objects.create(user=kwargs["instance"]) diff --git a/account/signals.py b/account/signals.py index 0d90abf2..00d562e8 100644 --- a/account/signals.py +++ b/account/signals.py @@ -1,7 +1,7 @@ import django.dispatch -user_signed_up = django.dispatch.Signal(providing_args=["user"]) +user_signed_up = django.dispatch.Signal(providing_args=["user", "form"]) user_sign_up_attempt = django.dispatch.Signal(providing_args=["username", "email", "result"]) signup_code_sent = django.dispatch.Signal(providing_args=["signup_code"]) signup_code_used = django.dispatch.Signal(providing_args=["signup_code_result"]) diff --git a/account/tests/test_views.py b/account/tests/test_views.py index 902f9072..11c631f9 100644 --- a/account/tests/test_views.py +++ b/account/tests/test_views.py @@ -2,17 +2,23 @@ from django.test.client import RequestFactory from django.utils import unittest -from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.models import AnonymousUser, User from account.views import SignupView, LoginView -class SignupDisabledView(SignupView): +class SignupEnabledView(SignupView): - def disabled(self): + def is_open(self): return True +class SignupDisabledView(SignupView): + + def is_open(self): + return False + + class LoginDisabledView(LoginView): def disabled(self): @@ -26,18 +32,33 @@ def setUp(self): def test_get(self): request = self.factory.get(reverse("account_signup")) - response = SignupView.as_view()(request) + request.user = AnonymousUser() + response = SignupEnabledView.as_view()(request) self.assertEqual(response.status_code, 200) def test_get_disabled(self): request = self.factory.get(reverse("account_signup")) + request.user = AnonymousUser() response = SignupDisabledView.as_view()(request) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template_name, "account/signup_closed.html") def test_post_disabled(self): request = self.factory.post(reverse("account_signup")) + request.user = AnonymousUser() response = SignupDisabledView.as_view()(request) - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template_name, "account/signup_closed.html") + + def test_post_successful(self): + post = {"username": "user", "password": "pwd", + "password_confirm": "pwd", "email": "info@example.com"} + request = self.factory.post(reverse("account_signup"), post) + request.user = AnonymousUser() + response = SignupEnabledView.as_view()(request) + self.assertEqual(response.status_code, 302) + user = User.objects.get(username="user") + self.asserEqual(user.email, "info@example.com") class LoginViewTestCase(unittest.TestCase): @@ -50,7 +71,8 @@ def test_get(self): request.user = AnonymousUser() response = LoginView.as_view()(request) self.assertEqual(response.status_code, 200) - + self.assertEqual(response.template_name, ["account/login.html"]) + def test_get_disabled(self): request = self.factory.get(reverse("account_login")) request.user = AnonymousUser() diff --git a/account/views.py b/account/views.py index 9458ad8b..a881bcc7 100644 --- a/account/views.py +++ b/account/views.py @@ -5,7 +5,7 @@ from django.template.loader import render_to_string from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ -from django.views.generic.base import TemplateResponseMixin, View, TemplateView +from django.views.generic.base import TemplateResponseMixin, View from django.views.generic.edit import FormView from django.contrib import auth, messages @@ -33,15 +33,15 @@ class SignupView(FormView): messages = { "email_confirmation_sent": { "level": messages.INFO, - "text": _("Confirmation email sent to %(email)s") + "text": _("Confirmation email sent to %(email)s.") }, "logged_in": { "level": messages.SUCCESS, - "text": _("Successfully logged in as %(user)s") + "text": _("Successfully logged in as %(user)s.") }, "invalid_signup_code": { "level": messages.WARNING, - "text": _("The code %(code)s is invalid") + "text": _("The code %(code)s is invalid.") } } @@ -56,10 +56,17 @@ def get(self, *args, **kwargs): return self.closed() return super(SignupView, self).get(*args, **kwargs) + def post(self, *args, **kwargs): + if not self.is_open(): + return self.closed() + return super(SignupView, self).post(*args, **kwargs) + def get_initial(self): initial = super(SignupView, self).get_initial() if self.signup_code: initial["code"] = self.signup_code.code + if self.signup_code.email: + initial["email"] = self.signup_code.email return initial def get_context_data(self, **kwargs): @@ -86,20 +93,21 @@ def form_valid(self, form): if settings.ACCOUNT_EMAIL_CONFIRMATION_REQUIRED: new_user.is_active = False new_user.save() + self.create_account(new_user, form) email_kwargs = {"primary": True} if self.signup_code: self.signup_code.use(new_user) - if self.signup_code.email and form.cleaned_data["email"] == self.signup_code.email: + if self.signup_code.email and new_user.email == self.signup_code.email: email_kwargs["verified"] = True email_confirmed = True - EmailAddress.objects.add_email(new_user, form.cleaned_data["email"], **email_kwargs) + EmailAddress.objects.add_email(new_user, new_user.email, **email_kwargs) self.after_signup(new_user, form) if settings.ACCOUNT_EMAIL_CONFIRMATION_REQUIRED and not email_confirmed: response_kwargs = { "request": self.request, "template": self.template_name_email_confirmation_sent, "context": { - "email": form.cleaned_data["email"], + "email": new_user.email, "success_url": self.get_success_url(), } } @@ -122,7 +130,7 @@ def form_valid(self, form): "user": user_display(new_user) } ) - return super(SignupView, self).form_valid(form) + return redirect(self.get_success_url()) def get_success_url(self): return default_redirect(self.request, settings.ACCOUNT_SIGNUP_REDIRECT_URL) @@ -136,8 +144,8 @@ def create_user(self, form, commit=True, **kwargs): if username is None: username = self.generate_username(form) user.username = username - user.email = form.cleaned_data["email"].strip().lower() - password = form.cleaned_data.get("password1") + user.email = form.cleaned_data["email"].strip() + password = form.cleaned_data.get("password") if password: user.set_password(password) else: @@ -146,12 +154,15 @@ def create_user(self, form, commit=True, **kwargs): user.save() return user + def create_account(self, new_user, form): + return Account.create(request=self.request, user=new_user) + def generate_username(self, form): raise NotImplementedError("Unable to generate username by default. " "Override SignupView.generate_username in a subclass.") def after_signup(self, user, form): - signals.user_signed_up.send(sender=SignupForm, user=user) + signals.user_signed_up.send(sender=SignupForm, user=user, form=form) def login_user(self, user): # set backend on User object to bypass needing to call auth.authenticate @@ -222,9 +233,8 @@ def get_redirect_field_name(self): def login_user(self, form): auth.login(self.request, form.user) - self.request.session.set_expiry( - 60*60*24*365*10 if form.cleaned_data.get("remember") else 0 - ) + expiry = settings.ACCOUNT_REMEMBER_ME_EXPIRY if form.cleaned_data.get("remember") else 0 + self.request.session.set_expiry(expiry) class LogoutView(TemplateResponseMixin, View): @@ -254,7 +264,7 @@ class ConfirmEmailView(TemplateResponseMixin, View): messages = { "email_confirmed": { "level": messages.SUCCESS, - "text": _("You have confirmed %(email)s") + "text": _("You have confirmed %(email)s.") } } @@ -273,8 +283,6 @@ def get(self, *args, **kwargs): def post(self, *args, **kwargs): self.object = confirmation = self.get_object() - if confirmation.email_address.user != self.request.user: - raise Http404() confirmation.confirm() user = confirmation.email_address.user user.is_active = True @@ -450,7 +458,7 @@ def get_context_data(self, **kwargs): def form_valid(self, form): user = self.get_user() - user.set_password(form.cleaned_data["password1"]) + user.set_password(form.cleaned_data["password"]) user.save() if self.messages.get("password_changed"): messages.add_message( @@ -504,24 +512,12 @@ def get_initial(self): initial = super(SettingsView, self).get_initial() if self.primary_email_address: initial["email"] = self.primary_email_address.email - initial["timezone"] = self.request.user.account.timezone - initial["language"] = self.request.user.account.language + initial["timezone"] = self.request.user.account.timezone + initial["language"] = self.request.user.account.language return initial def form_valid(self, form): - # @@@ handle multiple emails per user - if not self.primary_email_address: - EmailAddress.objects.add_email(self.request.user, form.cleaned_data["email"], primary=True) - else: - if form.cleaned_data["email"] != self.primary_email_address.email: - email_address = EmailAddress.objects.add_email(self.request.user, form.cleaned_data["email"]) - email_address.set_as_primary() - - account = self.request.user.account - account.timezone = form.cleaned_data["timezone"] - account.language = form.cleaned_data["language"] - account.save() - + self.update_settings(form) if self.messages.get("settings_updated"): messages.add_message( self.request, @@ -530,5 +526,38 @@ def form_valid(self, form): ) return redirect(self.get_success_url()) - def get_success_url(self): - return default_redirect(self.request, settings.ACCOUNT_SETTINGS_REDIRECT_URL) + def update_settings(self, form): + self.update_email(form) + self.update_account(form) + + def update_email(self, form): + user = self.request.user + # @@@ handle multiple emails per user + email = form.cleaned_data["email"].strip() + if not self.primary_email_address: + user.email = email + EmailAddress.objects.add_email(self.request.user, email, primary=True) + user.save() + else: + if email != self.primary_email_address.email: + user.email = email + self.primary_email_address.email = email + user.save() + self.primary_email_address.save() + + def update_account(self, form): + fields = {} + if "timezone" in form.cleaned_data: + fields["timezone"] = form.cleaned_data["timezone"] + if "language" in form.cleaned_data: + fields["language"] = form.cleaned_data["language"] + if fields: + account = self.request.user.account + for k, v in fields.iteritems(): + setattr(account, k, v) + account.save() + + def get_success_url(self, fallback_url=None): + if fallback_url is None: + fallback_url = settings.ACCOUNT_SETTINGS_REDIRECT_URL + return default_redirect(self.request, fallback_url) diff --git a/docs/installation.rst b/docs/installation.rst index ea5f383e..07b38474 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -4,9 +4,9 @@ Installation ============ -* To install django-user-accounts (no releases have been yet):: +* Install the development version:: - pip install django-user-accounts + pip install --extra-index-url=http://dist.pinaxproject.com/dev/ django-user-accounts * Add ``account`` to your ``INSTALLED_APPS`` setting:: @@ -53,4 +53,12 @@ django-appconf_ We use django-appconf for app settings. It is listed in ``install_requires`` and will be installed when pip installs. -.. _django-appconf: https://github.com/jezdez/django-appconf \ No newline at end of file +.. _django-appconf: https://github.com/jezdez/django-appconf + +pytz_ +----- + +pytz is used for handling timezones for accounts. This dependency is critical +due to its extensive dataset for timezones. + +.. _pytz: http://pypi.python.org/pypi/pytz/ \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst index 0cccbe83..9e835597 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -198,6 +198,6 @@ tables for django-user-accounts you will need to migrate the ALTER TABLE "account_emailaddress" ADD CONSTRAINT "account_emailaddress_user_id_email_key" UNIQUE ("user_id", "email"); ALTER TABLE "account_emailaddress" DROP CONSTRAINT "account_emailaddress_email_key"; -``ACCOUNT_EMAIL_UNIQUE = False`` will prevent duplicate email addresses per -user. +``ACCOUNT_EMAIL_UNIQUE = False`` will allow duplicate email addresses per +user, but not across users. diff --git a/setup.py b/setup.py index bae28687..afd065f6 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name = "django-user-accounts", - version = "1.0b1.dev1", + version = "1.0b1.dev4", author = "Brian Rosner", author_email = "brosner@gmail.com", description = "a Django user account app", @@ -13,7 +13,7 @@ packages = find_packages(), install_requires = [ "django-appconf==0.5", - "pytz==2012b" + "pytz==2012c" ], classifiers = [ "Development Status :: 4 - Beta",