Update
I tried replacing everything in my custom manager with the following:
def create_user(self, username, email):
return self.model._default_manager.create(username=username)
And that throws an error. I then tried returning the User from my custom user manager and I get 'Cannot assign "": "UserSocialAuth.user" must be a "Barbuser" instance.' thrown from associate_user. It comes from the bowels of django.db.models.fields.related.py. Basically, I'm stuck with knowing how to correctly create users from my custom model mgr. I was going directly off of the docs which lead me to copying everything from django's built in ModelManager. Help?
Update
I'm having trouble configuring django-social-auth. I've been at this for 3-4 days and I'm getting ready to throw in the towel. I have a working existing user registration app installed and I then installed and followed along with the docs on django-social-auth github site. I added the following to my settings.py
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'polls',
'barbuser',
'social_auth',
)
AUTHENTICATION_BACKENDS = (
'social_auth.backends.facebook.FacebookBackend',
'django.contrib.auth.backends.ModelBackend',
)
FACEBOOK_APP_ID = os.environ.get('FACEBOOK_APP_ID')
FACEBOOK_API_SECRET = os.environ.get('FACEBOOK_SECRET')
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'social_auth.context_processors.social_auth_by_type_backends',
)
#SOCIAL_AUTH_ENABLED_BACKENDS = ('facebook',)
SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user'
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/profile/'
LOGIN_ERROR_URL = '/login-error/'
SOCIAL_AUTH_USER_MODEL = 'barbuser.Barbuser'
My models.py looks like:
from datetime import date
from django.db import models
from django.contrib.auth.models import User
from django.db.models import BooleanField
from django.db.models.fields import DateTimeField
from django.utils import timezone
from django.utils.crypto import get_random_string
class BarbuserManager(models.Manager):
#classmethod
def normalize_email(cls, email):
"""
Normalize the address by converting the domain part of the email address to lowercase.
"""
email = email or ''
try:
email_name, domain_part = email.strip().rsplit('#', 1)
except ValueError:
pass
else:
email = '#'.join([email_name, domain_part.lower()])
return email
def create_user(self, username, email=None, password=None):
"""
Creates and saves a User with the given username, email and password.
"""
email = 'you#email.com' if email.strip() == '' else email
now = timezone.now()
if not username:
raise ValueError('The given username must be set')
email = BarbuserManager.normalize_email(email)
user = User(username=username, email=email,
is_staff=False, is_active=True, is_superuser=False,
last_login=now, date_joined=now)
user.set_password(password)
user.save()
barbuser = Barbuser(user=user, birthday=date.today(), last_login=user.last_login, name=username)
barbuser.save()
return barbuser
def create_superuser(self, username, email, password):
u = self.create_user(username, email, password)
u.is_staff = True
u.is_active = True
u.is_superuser = True
u.save(using=self._db)
return u
def make_random_password(self, length=10,
allowed_chars='abcdefghjkmnpqrstuvwxyz'
'ABCDEFGHJKLMNPQRSTUVWXYZ'
'23456789'):
""" Generates a random password with the given length and given allowed_chars. Note that the default value of allowed_chars does not have "I" or "O" or letters and digits that look similar -- just to avoid confusion. """
return get_random_string(length, allowed_chars)
def get_by_natural_key(self, username):
return self.get(username=username)
class Barbuser(models.Model):
user = models.OneToOneField(User)
username = models.CharField(max_length=200)
last_login = DateTimeField(blank=True)
is_active = BooleanField(default=True)
birthday = models.DateField()
name = models.CharField(max_length=200)
objects = BarbuserManager()
def __init__(self, *args, **kwargs):
me = super(Barbuser, self).__init__(*args, **kwargs)
barbuser = me
return me
def __unicode__(self):
return self.name
def is_authenticated(self):
return self.user.is_authenticated()
I've updated my urls.py to include 'social_auth.urls' and after authentication the user is redirected to ViewProfile view from my views.py:
# Create your views here.
from barbuser.forms import RegistrationForm, LoginForm
from barbuser.models import Barbuser
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
def create_Barbuser(form):
user = User.objects.create_user(form.cleaned_data['username'], form.cleaned_data['email'], form.cleaned_data['password'])
user.save()
barbuser = Barbuser(user=user, name=form.cleaned_data['name'], birthday=form.cleaned_data['birthday'])
barbuser.save()
def process_form(form, request_context):
if form.is_valid():
create_Barbuser(form)
return HttpResponseRedirect('/profile/')
else:
return render_to_response('register.html', {'form': form}, context_instance=request_context)
def render_blank_registration_form(request):
'''When the user is not submitting the form, show them the blank registration form.'''
form = RegistrationForm()
context = {'form': form}
return render_to_response('register.html', context, context_instance=RequestContext(request))
def BarbuserRegistration(request):
"""
Handles the registration of new Barbwire users.
"""
if request.user.is_authenticated():
return HttpResponseRedirect('/profile/')
if request.method == "POST":
return process_form(RegistrationForm(request.POST), RequestContext(request))
else:
return render_blank_registration_form(request)
def LoginRequest(request):
'''
Handles Login requests.
'''
if request.user.is_authenticated():
return HttpResponseRedirect('/profile/')
if request.method == 'POST':
form = LoginForm(request.POST)
if form.is_valid():
username = form.cleaned_data['username']
password = form.cleaned_data['password']
barbuser = authenticate(username=username, password=password)
if barbuser is not None:
login(request, barbuser)
return HttpResponseRedirect('/profile/')
else:
return render_to_response('login.html', {'form' : form}, context_instance=RequestContext(request))
else:
return render_to_response('login.html', {'form' : form}, context_instance=RequestContext(request))
else:
form = LoginForm()
return render_to_response('login.html', {'form' : form}, context_instance=RequestContext(request))
def LoginError(request):
return render_to_response('login.html', {'form' : LoginForm()}, context_instance=RequestContext(request))
def LogoutRequest(request):
logout(request)
return HttpResponseRedirect('/')
def ViewProfile(request):
if not request.user.is_authenticated():
return HttpResponseRedirect('/login/')
else:
return render_to_response('profile.html',{'barbuser' : request.user.barbuser }, context_instance=RequestContext(request))
My problem is 2-fold. When I add this extra stuff in my models.py:
def facebook_extra_values(sender, user, response, details, **kwargs):
return False
from social_auth.signals import pre_update
from social_auth.backends.facebook import FacebookBackend
pre_update.connect(facebook_extra_values, sender=FacebookBackend)
I get errors on server startup: assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.class._name_, to, RECURSIVE_RELATIONSHIP_CONSTANT)
AssertionError: ForeignKey(None) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string 'self'
When I remove it I can go thru the login with facebook flow but I get: AttributeError at /complete/facebook/
'NoneType' object has no attribute 'extra_data'
I'm not sure what I'm missing or where I've gone wrong. could somebody help explain where I'm going wrong?
Update
I've traced the problem in debug and apparently I'm getting an IntegrityError in social_auth.backends.pipeline.social.py in the associate_user function when it tries "UserSocialAuth.objects.create". It then falls back into an Except block that calls social_auth_user() and this function returns None for the social_user. The IntegrityError I'm getting is:
insert or update on table "social_auth_usersocialauth" violates foreign key constraint "social_auth_usersocialauth_user_id_fkey"
DETAIL: Key (user_id)=(17) is not present in table "auth_user".
I'm not familiar enough to know how, where or why to associate a social_user with the user created in my CustomUserManager in my models.py. Also I've removed the facebook_extra_values, extra imports, and the preupdate.connect stuff from the bottom of my models.py since I really don't understand what it does or what it's for. I was merely copying things from the example app trying to fix my first problem with the missing association. Help?
Update
The first problem — the ForeignKey error — is caused by a circular import. The solution is easy and follows convention. Take that signal registering code block at the end of models.py and move it to a new file called barbuser/signals.py. Then in barbuser/__init__.py put the line, from .signals import *
I haven't run your code far enough to get your second error — 'NoneType' object has no attribute 'extra_data' — but I found a couple other issues.
Remove the SOCIAL_AUTH_ENABLED_BACKENDS setting from settings.py. I'm not sure where you got that from, but it's not in the documentation for the current social-auth 0.7.0. It's not in the social_auth code either. Probably an old setting.
You reference a non-existent context processor in settings.py (social_auth_login_redirect). Again, maybe an old function. Make sure you are running the newest social-auth (available through PyPi with pip install django-social-auth). Also, use only the context processors you need, never all of them. (Context processors add variables to the template context. If you aren't using the social_auth variable in your templates then you don't need any of the social-auth context processors.) This is important because two of the processors conflict with each other. From the documentation,
social_auth_backends and social_auth_by_type_backends don't play nice
together.
Make sure you've run ./manage.py syncdb to setup the database for the social-auth Model. If there is no database table for the model, that would cause "UserSocialAuth.objects.create()" to break.
Last thought, Django doesn't like the type of thing you are doing defining your own User Model. It's best to leave auth.User alone and make a UserProfile.
UPDATE
Check your database structure; I suspect the indexes are incorrect. Better yet, just delete the three social_auth_* tables and syncdb again. The UserSocialAuth table has a ForeignKey Constraint to the User Model. It should be mapping "user_id" to "Barbuser.id" but if the table was created before you set the settings.py/SOCIAL_AUTH_USER_MODEL value, then the table will be forever broken because SocialAuth defaulted to Django's Auth.User when the initial SQL was run. Make sense? Anyway, just recreate the tables now that you have Social Auth configured.
Note: Your recent experiment using self.model._default_manager.create() isn't accomplishing anything. "self" is a BarbuserManager; "model" is a Barbuser; "_default_manager" is back to a BarbuserManager. Why? "Barbuser.objects" is the default manager for the Model.
Oh, and you had it correct the first time. BarbuserManager.create_user() needs to return a Barbuser.
Related
I'm just trying start work with Permissions to learn how thats work.
In shell i have empty set() result from user_test.get_all_permissions(). I see this question, is the same of that, but i can't find what is missing im my code:
Edit
Now seeing in class ModelBackend, a particular part confuses my mind: ...get_all_permissions(),...allow an object to be passed as a parameter for object-specific permissions, but this backend does not implement them other than returning an empty set of permissions if obj is not None
And bellow in docs:
get_all_permissions(user_obj, obj=None)
Returns the set of permission strings the user_obj has, including both user permissions and group permissions. Returns an empty set if is_anonymous or is_active is False.
My user have is_active flag=true, even though user_test.get_all_permissions() return empty set().
In this question a comment call my atention:
"You need to implement a get_all_permissions in your User model..."
I'm lost in this general understanding that part of Permissions concept.
I created Custom User from AbstractBaseUser, with follow code:
settings.py
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users',
]
AUTH_USER_MODEL = 'users.User'
AUTHENTICATION_BACKENDS = [
'users.backends.CustomModelBackend',
]
...
models.py
class UserManager(BaseUserManager):
def create_user(self, email, username, phone, password = None):
...
def create_superuser(self, email, username, phone, password = None):
...
class User(AbstractBaseUser, PermissionsMixin):
...
objects = UserManager()
...
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
return True
backends.py
class CustomModelBackend(backends.ModelBackend):
def user_can_authenticate(self, user):
return True
In shell i try something like:
>>> from users.models import User
>>> user_test = User.objects.get(email="user#example.com")
>>> user_test.get_all_permissions()
set()
That user is superuser. And i test it to regular user to
EDIT
I realized that I might need to implement this in the backend, I don't know if it's the correct path, but making the following change I get the following error:
backends.py edit
from django.contrib.auth import backends
def _user_get_permissions(user, obj, from_name):
permissions = set()
name = 'get_%s_permissions' % from_name
for backend in backends.get_backends():
if hasattr(backend, name):
permissions.update(getattr(backend, name)(user, obj))
return permissions
class CustomModelBackend(backends.ModelBackend):
def user_can_authenticate(self, user):
return True
def _get_user_permissions(self, user_obj):
return user_obj.user_permissions.all()
def get_all_permissions(self, obj=None):
return _user_get_permissions(self, obj, 'all')
The error:
...
line 198, in _user_get_permissions
permissions.update(getattr(backend, name)(user, obj))
TypeError: get_all_permissions() takes from 1 to 2 positional arguments but 3 were given
What is needed to check that permissions?
For superuser this list would be empty. On has_perm method there is IF for superuser and its always return True for superuser
In my case, I had my custom User class, and I was trying to use djago-jazzmin for admin panel. I was getting error because I didn't have get_all_permissions method on my custom User model. Implementing stated method as below solved my problem and I was able to successfully load jazzmin admin:
def get_all_permissions(user=None):
if user.is_superadmin:
return set()
I have a class-based view that subclasses LoginView.
from django.contrib.auth.views import LoginView
class CustomLoginView(LoginView):
def get_success_url(self):
url = self.get_redirect_url()
return url or reverse_lazy('knowledgebase:user_home', kwargs={
'username':self.request.user.username,
})
I want to override the error message if a user's email is not yet active because they have to click a link sent to their email address. The current default message looks like this:
Instead of saying:
Please enter a correct email address and password. Note that both
fields may be case-sensitive.
I want to say something to the effect of:
Please confirm your email so you can log in.
I tried:
accounts/forms.py
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext as _
class PickyAuthenticationForm(AuthenticationForm):
def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(
_("Please confirm your email so you can log in."),
code='inactive',
)
accounts/views.py
class CustomLoginView(LoginView): # 1. <--- note: this is a class-based view
form_class = PickyAuthenticationForm # 2. <--- note: define form here?
def get_success_url(self):
url = self.get_redirect_url()
return url or reverse_lazy('knowledgebase:user_home', kwargs={
'username':self.request.user.username,
})
The result is absolutely no effect when I try to log in with a user that does exist, but hasn't verified their email address yet.
AuthenticationForm docs.
Method - 1
Django uses ModelBackend as default AUTHENTICATION_BACKENDS and which does not authenticate the inactive users.
This is also stated in Authorization for inactive users sections,
An inactive user is one that has its is_active field set to False. The
ModelBackend and RemoteUserBackend authentication backends prohibits
these users from authenticating. If a custom user model doesn’t have
an is_active field, all users will be allowed to authenticate.
So, set AllowAllUsersModelBackend as your AUTHENTICATION_BACKENDS in settings.py
# settings.py
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']
How much does it affect my Django app?
It doesn't affect anything other than the authentication. If we look into the source code of AllowAllUsersModelBackend class we can see it just allowing the inactive users to authenticate.
Method - 2
Personally, I don't recommend this method since method-1 is the Django way of tackling this issue.
Override the clean(...) method of PickyAuthenticationForm class and call the AllowAllUsersModelBackend backend as,
from django.contrib.auth.backends import AllowAllUsersModelBackend
class PickyAuthenticationForm(AuthenticationForm):
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username is not None and password:
backend = AllowAllUsersModelBackend()
self.user_cache = backend.authenticate(self.request, username=username, password=password)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
if not user.is_active:
raise forms.ValidationError(
"Please confirm your email so you can log in.",
code='inactive',
)
Result Screenshot
You need to use AllowAllUsersModelBackend
https://docs.djangoproject.com/en/3.0/ref/contrib/auth/#django.contrib.auth.backends.AllowAllUsersModelBackend
Here you will get instruction for setting custom backend
https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#specifying-authentication-backends
Hope it helps.
I'm not convinced setting up a custom backend is the solution when I simply want to override a message. I did a temporary fix by defining form_invalid. Yes it's hacky but for now, it'll do the trick. Doubt this will help anyone but it was interesting to discover form.errors. Maybe someone can build off this to solve their specific problem.
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
#TODO: This is EXTREMELY HACKY!
if form.errors:
email = form.cleaned_data.get('username')
if User.objects.filter(email=email, username=None).exists():
if len(form.errors['__all__']) == 1:
form.errors['__all__'][0] = 'Please confirm your email to log in.'
return self.render_to_response(self.get_context_data(form=form))
I am using django-rest-jwt for authentication in my app.
By default it user username field to autenticate a user but I want let the users login using email or username.
Is there any mean supported by django-rest-jwt to accomplish this.
I know the last option would be write my own login method.
No need to write a custom authentication backend or custom login method.
A Custom Serializer inheriting JSONWebTokenSerializer, renaming the 'username_field' and overriding def validate() method.
This works perfectly for 'username_or_email' and 'password' fields where the user can enter its username or email and get the JSONWebToken for correct credentials.
from rest_framework_jwt.serializers import JSONWebTokenSerializer
from django.contrib.auth import authenticate, get_user_model
from django.utils.translation import ugettext as _
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
class CustomJWTSerializer(JSONWebTokenSerializer):
username_field = 'username_or_email'
def validate(self, attrs):
password = attrs.get("password")
user_obj = User.objects.filter(email=attrs.get("username_or_email")).first() or User.objects.filter(username=attrs.get("username_or_email")).first()
if user_obj is not None:
credentials = {
'username':user_obj.username,
'password': password
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
else:
msg = _('Account with this email/username does not exists')
raise serializers.ValidationError(msg)
In urls.py:
url(r'{Your url name}$', ObtainJSONWebToken.as_view(serializer_class=CustomJWTSerializer)),
Building on top of Shikhar's answer and for anyone coming here looking for a solution for rest_framework_simplejwt (since django-rest-framework-jwt seems to be dead, it's last commit was 2 years ago) like me, here's a general solution that tries to alter as little as possible the original validation from TokenObtainPairSerializer:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomJWTSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
credentials = {
'username': '',
'password': attrs.get("password")
}
# This is answering the original question, but do whatever you need here.
# For example in my case I had to check a different model that stores more user info
# But in the end, you should obtain the username to continue.
user_obj = User.objects.filter(email=attrs.get("username")).first() or User.objects.filter(username=attrs.get("username")).first()
if user_obj:
credentials['username'] = user_obj.username
return super().validate(credentials)
And in urls.py:
url(r'^token/$', TokenObtainPairView.as_view(serializer_class=CustomJWTSerializer)),
Found out a workaround.
#permission_classes((permissions.AllowAny,))
def signin_jwt_wrapped(request, *args, **kwargs):
request_data = request.data
host = request.get_host()
username_or_email = request_data['username']
if isEmail(username_or_email):
# get the username for this email by model lookup
username = Profile.get_username_from_email(username_or_email)
if username is None:
response_text = {"non_field_errors":["Unable to login with provided credentials."]}
return JSONResponse(response_text, status=status.HTTP_400_BAD_REQUEST)
else:
username = username_or_email
data = {'username': username, 'password':request_data['password']}
headers = {'content-type': 'application/json'}
url = 'http://' + host + '/user/signin_jwt/'
response = requests.post(url,data=dumps(data), headers=headers)
return JSONResponse(loads(response.text), status=response.status_code)
I check that whether the text that I received is a username or an email.
If email then I lookup the username for that and then just pass that to /signin_jwt/
authentication.py
from django.contrib.auth.models import User
class CustomAuthBackend(object):
"""
This class does the athentication-
using the user's email address.
"""
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
settings.py
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'app_name.authentication.CustomAuthBackend',
]
How it works:
If user try to authenticate using their username django will look at the ModelBackend class. However, if the user adds its email instead, django will try ModelBackend but will not find the logic needed, then will try the CustomAuthBackend class making it work the authentication.
Alternatively, this new DRF Auth project dj-rest-auth seems to provide support for log in by username or email through djangorestframework-simplejwt.
dj-rest-auth works better for authentication and authorization. By default dj-rest-auth provides - username, email and password fields for login. User can provide email and password or username and password. Token will be generated, if the provided values are valid.
If you need to edit these login form, extend LoginSerializer and modify fields. Later make sure to add new custom serializer to settings.py.
REST_AUTH_SERIALIZERS = {
'LOGIN_SERIALIZER': 'yourapp.customlogin_serializers.CustomLoginSerializer'
}
Configuring dj-rest-auth is bit tricky, since it has an open issue related to the refresh token pending. There is workaround suggested for that issue, so you can follow below links and have it configured.
https://medium.com/geekculture/jwt-authentication-in-django-part-1-implementing-the-backend-b7c58ab9431b
https://github.com/iMerica/dj-rest-auth/issues/97
If you use the rest_framework_simplejwt this is a simple mode. views.py
from rest_framework_simplejwt.tokens import RefreshToken
from django.http import JsonResponse
from rest_framework import generics
class EmailAuthToken(generics.GenericAPIView):
def post(self, request):
user_data = request.data
try:
user = authenticate(request, username=user_data['username_or_email'], password=user_data['password'])
if user is not None:
login(request, user)
refresh = RefreshToken.for_user(user)
return JsonResponse({
'refresh': str(refresh),
'access': str(refresh.access_token),
}, safe=False, status=status.HTTP_200_OK)
else:
return JsonResponse({
"detail": "No active account found with the given credentials"
}, safe=False, status=status.HTTP_200_OK)
except:
return Response({'error': 'The format of the information is not valid'}, status=status.HTTP_401_UNAUTHORIZED)
I'm trying to provide a user registration form without requiring a username to be manually entered. I setup an authentication backend so that users can authenticate with an email address and that works because I can login with the Django admin user. The idea is that my app would create a SHA1 hash of the email address to be stored as the Django username and the user would never see this.
I removed the username field from my registration html template, but I'm not sure how or where I should be generating the username programmatically. I think it should be in the clean_username method of the RegistrationForm, but that method doesn't get called when I use a template without the username field.
Any help would certainly be appreciated.
I got it to work. I don't feel good about having to modify the registration's view method and the default registration backend directly. I'd prefer to have those changes in my own code and am still working to make those changes, but this does indeed work.
Here's how I did it:
Created a custom registration backend called RegBackend that generates a sha1 hash based on the email address and then stores the hexdigest as the username, finally returning a User object.
Map register in urls.py to the new RegBackend that I created in step 1
Modified the django-registration's view register method to create a random username so that the form validates, but I never persist the random username. I copied the request.POST dictionary and set this random username to one of the copy's dictionary keys called data['username']. Then I use the data variable when creating an instance of the form_class. Calling is_valid() on the form would return false because I removed the username from the template, but Django requires a username for registration, so I needed to supply it something.
I didn't use the sha1 hash for the random username because the Django username can only be 30 characters in length while the sha1 hash is 40. Oddly, the custom registration backend didn't complain and can store the sha1 hash, but the form would generate errors because of the length when submitted.
_init_.py (I copied the existing DefaultBackend and modified with the SHA1 portion)
from django.conf import settings
from django.contrib.sites.models import RequestSite
from django.contrib.sites.models import Site
from registration import signals
from registration.forms import RegistrationForm
from registration.models import RegistrationProfile
import hashlib
class RegBackend(object):
def register(self, request, **kwargs):
hash_user = hashlib.sha1()
hash_user.update(kwargs['email'])
username, email, password = hash_user.hexdigest(), kwargs['email'], kwargs['password1']
if Site._meta.installed:
site = Site.objects.get_current()
else:
site = RequestSite(request)
new_user = RegistrationProfile.objects.create_inactive_user(username, email,
password, site)
signals.user_registered.send(sender=self.__class__,
user=new_user,
request=request)
return new_user
#omitted other code from DefaultBackend that I didn't modify
urls.py
url(r'^register/$', register, {'backend': 'registration.backends.default.RegBackend', 'form_class': UserRegistrationForm}, name='registration_register'),
registration/views.py
def register(request, backend, success_url=None, form_class=None,
disallowed_url='registration_disallowed',
template_name='registration/registration_form.html',
extra_context=None):
backend = get_backend(backend)
if not backend.registration_allowed(request):
return redirect(disallowed_url)
if form_class is None:
form_class = backend.get_form_class(request)
if request.method == 'POST':
# I added the next two lines
data = request.POST.copy()
data['username'] = ''.join([choice(letters) for i in xrange(30)])
form = form_class(data=data, files=request.FILES)
if form.is_valid():
new_user = backend.register(request, **form.cleaned_data)
if success_url is None:
to, args, kwargs = backend.post_registration_redirect(request, new_user)
return redirect(to, *args, **kwargs)
else:
return redirect(success_url)
else:
form = form_class()
if extra_context is None:
extra_context = {}
context = RequestContext(request)
for key, value in extra_context.items():
context[key] = callable(value) and value() or value
return render_to_response(template_name,
{'form': form},
context_instance=context)
You might be thinking of subclassing RegistrationForm and then overriding the clean method (I don't like that though, since the clean method has a clear purpose, see https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#overriding-the-clean-method)
A simpler solution might be to prompt the user to enter their email address, then process that form to generate your username, and pass that value to the register view as an extra context . Then instead of removing the username field, assign the hashed value to it and hide it in your template. That way, you won't have to mess around with django-registration itself.
See https://bitbucket.org/ubernostrum/django-registration/src/fad7080fe769/registration/views.py for more info on the extra_context argument.
I'm working on an app that has three "profile" models types. Each of these models has a foreign key to the contrib.auth.models User model.
I would like to have a single sign on for each model type and provide a redirect via classmethod depending on which "profile" model type is related to the logged in user.
Here's some pseudo-code to illustrate what I would like to do:
from django.contrib.auth import authenticate, login
from django.http import HttpResponse, HttpresponseRedirect
from lib.forms import Loginform #extends the built-in AuthenticationForm
def login_user(request):
if request.method == 'POST':
form = LoginForm(data=request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
user = authenticate(username=cleaned_data.get('username'), \
password=cleaned_data.get('password'))
if user:
login(request, user)
#Determine the content type of the model related to the user
#get the correct redirect value from an #classmethod
#called: after_login_redirect for that type and...
return HttpResponseRedirect(klass.after_login_redirect())
else:
response = form.errors_as_json()
return HttpResponse(json.dumps(response, ensure_ascii=False), \
mimetype='application/json')
Is it possible to leverage the ContentTypes framework to do this? or am I better off just writing a class resolver that loops over an array of the "profile" classes? I'm not seeing a way I can do this with ContentTypes from the User class, unless someone knows a workaround.
Thanks in advance,
Brandon
[SOLVED]
So, what I ended up doing was creating a UserContentType junction model that looks like this:
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
class UserContentType(models.Model):
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
Then, I made a pre-save signal to fire a get or create if the instance of one of the three "profile" models I have doesn't have an id:
from django.contrib.contenttypes.models import ContentType
from lib.models import UserContentType
def set_content_type_for_user(sender, **kwargs):
instance = kwargs.get('instance')
content_type = ContentType.objects.get_for_model(instance)
user = instance.user
if not instance.id:
user_content_type, \
created = UserContentType.objects.get_or_create(user=user, \
content_type=content_type, \
defaults={'user' : user, 'content_type' : content_type})
Then, in my custom login view which is posted to via ajax, I can get the content type associated with the User instance and get the URL to redirect to from the 'after_login_redirect()' classmethod on that content type. :)
def login_user(request):
if request.method == 'POST':
form = LoginForm(data=request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
user = authenticate(username=cleaned_data.get('username', ''),
password=cleaned_data.get('password', ''))
if user:
login(request, user)
user_content_type = UserContentType.objects.get(user=user)
redirect_path =
user_content_type.content_type.\
model_class().after_login_redirect_path()
response = {'redirect_path' : redirect_path}
else:
response = form.errors_as_json()
return HttpResponse(json.dumps(response, \
ensure_ascii=False), mimetype='application/json')
This way, I don't have to monkeypatch User. I hope this helps someone out. If there's a way I can improve on this, I'd love to hear some ideas.
Thanks,
Brandon