I'm working on a project in django which calls for having separate groups of users in their own username namespace.
So for example, I might have multiple "organizations", and username should only have to be unique within that organization.
I know I can do this by using another model that contains a username/organization id, but that still leaves this useless (and required) field on the defualt django auth User that I would have to populate with something.
I've already written by own auth backend that authenticates a user against LDAP. However, as I mentioned before, I am still stuck with the problem of how to populate / ignore the username field on the default django user.
Is there a way to drop the uniqueness constraint for the username for Django auth users?
I'm not sure if this is exactly what you're looking for, but I think you could use a hack similar to what is in this answer.
The following code works, as long as it is in a place that gets executed when Django loads your models.
from django.contrib.auth.models import User
User._meta.get_field('username')._unique = False
Note that this won't change the database unique constraint on the auth_user table if it has been already been created. Therefore you need to make this change before you run syncdb. Alternatively, if you don't want to recreate your auth_user table, you could make this change and then manually alter the database table to remove the constraint.
What you can do is extend the User model. For the User table, generate a username (e.g. A_123, A_345) that will not be displayed at all in the site.
Then create a new model that extends User.
class AppUser(User):
username = models.CharField...
organization = models.CharField...
You then create a custom authentication backend that use AppUser instead of the User object.
I have not personally been required to find a solution to this, but one way to tackle this (from an SAAS perspective) would be to prefix the username with an organizational identifier (presuming unique organizations). For example: subdomain.yoursite.com would equate to a user with the username: subdomain_username. You would just have to code some business logic on login to a subdomain to tack that onto the username.
I also suffered with this problem. I was doing a project where I had to use email and mobile no. as login fields but none of them should be unique because their were different types of users and a user can have more than one user entity and also the project required only one auth user table (Hectic right!).
So I extended AbstractBaseUser class where I could change the USERNAME_FIELD attribute. Here's how :-
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import PermissionsMixin
# Custom model for User
class User(AbstractBaseUser, PermissionsMixin):
first_name = models.CharField(max_length=100, blank=False)
last_name = models.CharField(max_length=100, blank=True)
password = models.CharField(max_length=255, blank=False)
email = models.EmailField(max_length=255, blank=False)
mobile = models.CharField(max_length=12)
user_type = models.ForeignKey(UserTypes, on_delete=models.DO_NOTHING)
is_approved = models.BooleanField(default=False)
objects = UserManager()
# Here's the Catch
USERNAME_FIELD = 'id'
def get_full_name(self):
'''
Returns the first_name plus the last_name, with a space in between.
'''
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
'''
Returns the short name for the user.
'''
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
'''
Sends an email to this User.
'''
send_mail(subject, message, from_email, [self.email], **kwargs)
class Meta:
db_table = 'user'
Yes exactly, surprised? USERNAME_FIELD should be a unique field that's the constraint of this attribute. I couldn't use email or mobile no. as unique field.
Then I created a custom manager to remove the username field (reference = https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#abstractbaseuser)
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""
Creates and saves a User with the given email and password.
"""
if not email:
raise ValueError('The given email must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
This will do the trick.
I'm facing the exact same problem and I've been reading a lot (about how to solve that problem in 1.5) and I just thought of a much simpler solution. What if you just add a fixed-length prefix with the organization id to store the username?
I.e. Organization id = 115, chosen username = "john" and a fixed length of 6. So in the data base you store as username "000115_john".
When you do the login you just join the two parameters and try to authenticate with what Django provides. I'm not sure if the fixed length is strictly necessary but could avoid undesirable results if a user chooses a username with only numbers.
Related
I'm making a django website whose admin page looks like this
I want users(The content writer) to access only the Main Area(Events, Formats, Organisers) and not the whole thing. I haven't made any such user as Content Writer as of now. What are the permissions that should be given to that user. Should I make Groups for users(since there are basically 3 types of users as of now, i.e., Admin(Full Access), Content Writer(Limited Access), Basic Users(No access)) or should I just add one/two more variable(s) in my custom user model so that that user can access only the main area of the admin site.
After Giving these permissions can I extract the main area on a new html page and style it accordingly as:-
Events, Formats, Organisers, in Navbar
and the details of these page presented somewhat beautifully
models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
# Custom User Manager
class CustomUserManager(BaseUserManager):
def _create_user(self, email, password, first_name, last_name=None, **extra_fields):
if (not email):
raise ValueError("Email Must Be Provided")
if (not password):
raise ValueError("Password is not Provided")
user = self.model(
email=self.normalize_email(email),
first_name=first_name,
last_name=last_name,
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password, first_name, last_name=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_active', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, first_name, last_name, **extra_fields)
def create_superuser(self, email, password, first_name, last_name=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_active', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, first_name, last_name, **extra_fields)
# Custom user Model
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(db_index=True, unique=True, max_length=254)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255, null=True)
mobile = models.CharField(max_length=50)
address = models.CharField(max_length=250)
# profile_pic = models.ImageField(null=True, blank=True)
is_staff = models.BooleanField(default=True)
is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['first_name']
class Meta:
verbose_name = 'User'
verbose_name_plural = 'Users'
accounts.admin.py
from django.contrib import admin
from .models import User
# Register your models here.
class UserManager(admin.ModelAdmin):
list_display = ['first_name', 'email', 'mobile', 'is_staff', 'is_active', 'is_superuser']
admin.site.register(User, UserManager)
main.admin.py
from django.contrib import admin
from .models import *
# Register your models here.
class EventManager(admin.ModelAdmin):
list_display = [
'event_name',
'organiser_of_event',
'format_of_event',
'date_of_event',
'registration_fees',
'created_at',
'updated_at',
]
admin.site.register(Format)
admin.site.register(Organiser)
admin.site.register(Event, EventManager)
Is there any way of achieving the above? If so, please provide necessary links or the code or some tutorial for it
think you are almost there! Yes in django using the pre-built groups and permissions are really useful as it requires no further complicated permissions checking.
To give a brief description, you can create user groups, like you said:
Admin(Full Access), Content Writer(Limited Access), Basic Users(No access)
And create permissions. For example a permission may be called has_edit_user_access and this is assigned to admin and possibly content writer. You create as many permissions as you need and you assign them accordingly to the groups. Then the users gain the permissions for the groups they are in.
Here is a useful link that explains model level permissions (which is what I prefer) but there are many ways to do it, this article lists all the ways to use django groups and permissions. There is of course the official documentation but I think this guide does a better job.
To address your second point, you can of course create booleans on models for each permissions, but that is a less scalable. It really depends on the needs of your project!
I decided to work with a new database and in the same time change my custom user id field to UUID
class PersoUser(AbstractBaseUser):
id = models.UUIDField(
primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(
verbose_name="Email Adress", max_length=200, unique=True)
username = models.CharField(
verbose_name="username", max_length=200, unique=True)
first_name = models.CharField(verbose_name="firstname", max_length=200)
last_name = models.CharField(verbose_name="lastname", max_length=200)
date_of_birth = models.DateField(verbose_name="birthday")
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
objects = PersoUserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["date_of_birth", "username"]
def __str__(self):
return self.username
def has_perm(self, perm, obj=None):
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
return True
#property
def is_staff(self):
"Is the user a member of staff?"
# Simplest possible answer: All admins are staff
return self.is_admin
class PersoUserManager(BaseUserManager):
def create_user(self, username, email, date_of_birth, password=None):
if not username:
raise ValueError("Users must have a username ")
user = self.model(
username=username,
email=self.normalize_email(email),
date_of_birth=date_of_birth
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, email, date_of_birth, password=None):
user = self.create_user(
username,
email=email,
password=password,
date_of_birth=date_of_birth
)
user.is_admin = True
user.save(using=self._db)
return user
When attempting to create a super user threw the shell, after providing an email username psswd and date_of_birth i got the following error
django.db.utils.ProgrammingError: column "id" is of type integer but expression is of type uuid
....
HINT: You will need to rewrite or cast the expression.
thanks in advance
That error is indicative of the database structure not matching the Django model definition. (Or, alternatively, you're forcing a numeric id somewhere in your code.) Check the table definition in your Postgres database first. If the id column is numeric, then there's something wrong with your migrations:
If you still have the old user-creating migration around, you will need to undo it with migrate. If you don't, you'll likely have to modify your user table manually, or, probably easier still, empty the database.
You will need to create the migration creating the user model in a way that has an UUID column for the primary key, and then apply it.
Worked without editing with a fresh sqlite db, for PostgreSQL ( also fresh db ) I replaced id = models.UUIDField by ID= models.UUIDField and it's now working, anyone could explained why ?
I'm working on a project using Python(3.7) and Django(2.2) in which I have to implement multiple types of users as:
Personal Account - below 18
Personal Account - above 18
Parent Account
Coach Account
Admin
along with that, I also need to use email as the username field for login/authentication.
The strategy I'm trying to use is to build a custom base model as User inherited from AbstractBaseUser and also created a custom User Manager to make the email as username but it's not working.
Here's my complete model code:
class UserManager(BaseUserManager):
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
if not email:
raise ValueError('Users must have an email address')
now = timezone.now()
email = self.normalize_email(email)
user = self.model(
email=email,
is_staff=is_staff,
is_active=True,
is_superuser=is_superuser,
last_login=now,
date_joined=now,
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email=None, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
user = self._create_user(email, password, True, True, **extra_fields)
user.save(using=self._db)
return user
def generate_cid():
customer_number = "".join([random.choice(string.digits) for i in range(10)])
return customer_number
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(max_length=255, unique=True)
is_personal_above_18 = models.BooleanField(default=False)
is_personal_below_18 = models.BooleanField(default=False)
is_parent = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
last_login = models.DateTimeField(null=True, blank=True)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = []
objects = UserManager()
def get_absolute_url(self):
return "/users/%i/" % self.pk
def get_email(self):
return self.email
class PersonalAccountAbove18(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE,
primary_key=True, related_name='profile')
customer_id = models.BigIntegerField(default=generate_cid)
class PersonalAccountBelow18(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE,
primary_key=True, related_name='profile')
customer_id = models.BigIntegerField(blank=False)
class ParentAccount(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE,
primary_key=True, related_name='profile')
customer_id = models.BigIntegerField(default=generate_cid)
I'm confused about my approach and even it's also return an error when I run makemigrations as:
users.User.user_permissions: (fields.E304) Reverse accessor for 'User.user_permissions' clashes with reverse accessor for 'User.user_permissions'.
HINT: Add or change a related_name argument to the definition for 'User.user_permissions' or 'User.user_permissions'.
Update:
I removed the PermissionMixin and related_name attributes from child models and migrations are running now but it still require the username instead of email.
The errors in makemigrations, and later (after dropping PermissionsMixin) requiring username instead of email for authentication are hints that you have not set your custom model as the default user model to be used by the Django auth app. As a result, Django is using the auth.User model as the default user model and your one being added to the project like any other model. So both of the user models exist simultaneously with the auth.User being the default/active one used for authentication purposes.
In essence, edit your settings.py to add the following:
AUTH_USER_MODEL = '<your_app>.User'
Now Django will use your customized User model instead of the one from auth app (the default) and the auth.User model will be dropped. The auth app uses the get_user_model (django.contrib.auth.get_user_model) function to get the currently active user model for the project which checks for settings.AUTH_USER_MODEL, and by default the rest of the system (e.g. the admin app) also checks for this setting to get the current user model. So as long as you're using the auth app the above should suffice.
For using email as login, I recommend the django-allauth package. It takes care of all the heavy lifting and you can use it to login with email instead of username. Will Vincent has a write up on this at:
https://wsvincent.com/django-allauth-tutorial-custom-user-model/
Will also has a good write up on creating custom user models at:
https://wsvincent.com/django-custom-user-model-tutorial/
In short, he recommends subclassing AbstractUser. Here is an example from one of my projects where a user collects points throughout their journey on the site and I must record these points.
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
points = models.IntegerField(default=0)
def user_id(self):
return self.id.__str__()
I have used django user model and extended it to add a phone field. I was able to use either username or email to login but cannot access phone number field from extended user model.
models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phonenumber = models.CharField(max_length=10)
backends.py
class AuthenticationBackend(backends.ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
usermodel = get_user_model()
print(usermodel)
try:
user = usermodel.objects.get(Q(username__iexact=username) | Q(
email__iexact=username))
if user.check_password(password):
return user
except user.DoesNotExist:
pass
backend.py is what I used to implement login via username or email but I couldn't do it for phonenumber in extended user model.
There are two ways to solve your problem.
Extend the User model by subclassing it, and changing the default User model. I would recommend you go this route, since the Phone number is used for authentication.
Set related_name on Profile, and query the User model from that name.
For the first one, I recommend you check out the Django documentation on creating custom users.
There is also a pretty extensive tutorial on how to do this here.
If you want to go the easy route, you just need to set a reverse accessor on the Profile's phonenumber field.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phonenumber = models.CharField(max_length=10, related_name='profile')
You can then search users by their profile's phonenumbers:
class AuthenticationBackend(backends.ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
usermodel = get_user_model()
print(usermodel)
try:
user = usermodel.objects.get(Q(username__iexact=username) | Q(
email__iexact=username) | Q(profile__phonenumber__iexact=username)
if user.check_password(password):
return user
except user.DoesNotExist:
pass
Although this is not within the scope of your question, I think you should be aware of the following issues with your code:
Phone numbers are often more complex than a 10 character string. +31 6 12345678, is a valid phone number, but it's more than 10 characters. Check out this question for pointers on how to store and validate phone numbers.
If you use a field for authentication, make sure it's unique. The phonenumber field should actually be phonenumber = models.CharField(max_length=10, related_name='profile', unique=True)
Your usermodel.objects.get(...) query will return more than one user if there is a user with phone number '0123456789' and a different user with username '0123456789'. You should constrain usernames to not contain phone numbers, which is only possible by extending the default User class.
You can extend the AbstractUser and add the phonenumber on it.
class User(AbstractUser):
phonenumber = models.CharField(max_length=10)
USERNAME_FIELD = 'phonenumber'
Then you should specify the custom model as the default user model in settings.py
AUTH_USER_MODEL = 'app_name.User'
Customizing authentication in Django
I created an app authentication, code for models is,
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import BaseUserManager
from django.db import models
class AccountManager(BaseUserManager):
def create_user(self, email, password=None, **kwargs):
if not email:
raise ValueError('Users must have a valid email address.')
account = self.model(email=self.normalize_email(email))
account.set_password(password)
account.save()
return account
def create_superuser(self, email, password, **kwargs):
account = self.create_user(email, password, **kwargs)
account.is_admin = True
account.save()
return account
class Account(AbstractBaseUser):
email = models.EmailField(unique=True)
# username = models.CharField(max_length=40, unique=True)
first_name = models.CharField(max_length=40, blank=True)
last_name = models.CharField(max_length=40, blank=True)
is_admin = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects = AccountManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __unicode__(self):
return self.email
def get_full_name(self):
return ' '.join([self.first_name, self.last_name])
def get_short_name(self):
return self.first_name
apart from this the changes that i did was added authentication in INSTALLED_APPS and in settings added AUTH_USER_MODEL = 'authentication.Account'
When i create a super-user from shell, its working and inserting in db, but when i try to login into the django admin console,
initially the error was is_staff not found, which i added. Now it is showing invalid username and password for all. I tried that a couple of time generating new one each time. What am i doing wrong ?
EDIT
I am adding the dbs dump that i used to validate if the user was created
sqlite> select * from authentication_account;
1|pbkdf2_sha256$24000$ZRHCXpUI3w0G$fh4z9y5vmYAZ0FEiDr908dJD3ezw62nm3lpkZxUi/Es=||me#rahulbhola.in|||1|0|2016-05-28 21:15:59.292988|2016-05-28 21:15:59.415382
Still login in django defaut admin does not work
You added the is_staff later on which seems to be false by default, so when you migrated the field was created in database with the value false in the is_staff field.
Please make the changes as below and try
class AccountManager(BaseUserManager):
def create_user(self, email, password=None, **kwargs):
if not email:
raise ValueError('Users must have a valid email address.')
account = self.model(email=self.normalize_email(email))
account.set_password(password)
account.save(using=self.db)
return account
def create_superuser(self, email, password, **kwargs):
account = self.create_user(email, password, **kwargs)
account.is_staff = True
account.is_admin = True
account.save(using=self.db)
return account
Please not account.save(using=self.db) in the code above and account.is_staff = True in the create admin method.
Please try creating the new user again i prefer using the shell ( python manage.py shell)
from authentication.models import Account
user = Account()
user.create_superuser(username='some_username', password='some_password')
user.save()
Now that the user is created please try logging in using the django admin.
This should work. I had similar issues which is fixed and i am able to create user and login from django admin.
I am assuming that you have created the custom authentication backend already, if not please create
Hope this helps you.