I am building a Django web app where I want two things:
Use Django's in-built User model for Django's Admin app usage (store owner)
Use DRF's Token Auth on a custom User model that I will be naming "Customer" (store customer)
How do I keep both the Authentication systems for the above stated purposes. From what I have read every one asks to override the User model but I don't want to do that. Instead I want to keep both. What strategy should I take up?
PS: It might be me, but I am not able to find any solution for this in DRF's Documentation. If there is please do point me in the right direction.
Django does provide an option for using a custom user model. But you can have one and only one user model.
The process is fairly simple, create your own model inheriting django.contrib.auth.models.AbstractUser and specify the AUTH_USER_MODEL settings variable. The Django admin works pretty well with the "custom user model" concept. The DRF token model also uses the settings.AUTH_USER_MODEL var for its OneToOne relation. So, this is can be a viable solution.
To separate out the user types, you can either use a char field with choices representing the user type or use the existing Django groups mechanism. But, in both cases, you could still only have one user model.
For any specific details, you can have OneToOne relations with different models storing extra info.
Something like this would do,
from django.contrib.auth.models import AbstractUser
from model_utils.choices import Choices # Useful package
from django.utils.functional import cached_property
class User(AbstractUser):
USER_TYPES = Choices(
("store_owner", "Store Owner"),
("customer", "Customer"),
)
...hack...
user_type = models.CharField(choices=USER_TYPES)
...hack...
#cached_property
def is_store_owner(self):
return (
self.user_type == self.USER_TYPES.store_owner
and self.store_owner is not None
)
#cached_property
def is_customer(self):
return (
self.user_type == self.USER_TYPES.customer
and self.customer is not None
)
class StoreOwner(models.Model):
user = models.OneToOneField(
"yourapp.User",
related_name="store_owner",
)
# ...extra store owner details...
class Customer(models.Model):
user = models.OneToOneField(
"userapp.User",
related_name="customer",
)
# ...extra customer details...
Related
I am building an API that should have the following kind of users
super_user - create/manage admins
admin - manage events(model) and event participants
participants - participate in events, invited to events by admins
Additional i want to have each type of user to have phone number field
I tried
class SuperUser(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=20)
class Admin(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=20)
class Participant(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=20)
But gut is telling me its a wrong way to handle this. Can someone please help.
One possible solution is:
Have only one User Model with role field, which defines what user role is.
Create a User Group and add each group needed permissions.
Add User to User Group
Limit access using a Django REST Framework (later DRF) Permission Class.
Explanation:
Using only one user model is a more simple and flexible solution. You can query all users, or filtered by feature (like user role). Standart Django auth system expects one UserModel.
Read more about Django user groups. See "Django Permissions Docs #1" and "Django Groups Docs #2". Also useful is "User groups and permissions".
You need to create a group for each user role, and add needed permissions for each group. (Django has a default model permission, created automatically, look at the docs on the given links) or create the needed permission manually in the model definition.
Manually or using a script, add User to the needed group by defining his role when a user is created or manually by Django Admin interface.
Now everything should be ready for limited access by the user's role. You can easily limit access to the DRF View using a permission class. See more information in the "DRF Permission Docs".
Let's define our own:
from rest_framework.permissions import DjangoModelPermissions
# Using DjangoModelPermissions we can limit access by checking user permissions.
# Rights need only for CreateUpdateDelete actions.
class CUDModelPermissions(DjangoModelPermissions):
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': ['%(app_label)s.read_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
# Or you can inherit from BasePermission class and define your own rule for access
from rest_framework.permissions import BasePermission
class AdminsPermissions(BasePermission):
allowed_user_roles = (User.SUPERVISOR, User.ADMINISTRATOR)
def has_permission(self, request, view):
is_allowed_user = request.user.role in self.allowed_user_roles
return is_allowed_user
# ----
# on views.py
from rest_framework import generics
from .mypermissions import CUDModelPermissions, AdminsPermissions
class MyViewWithPermissions(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [CUDModelPermissions, ]
queryset = SomeModel.objects.all()
serializer_class = MyModelSerializer
You can add additional permission class to combine access limitation.
So in Django any user has a flag is_superuser that corresponds to your 'superuser'. So just use that - e.g. User.objects.create(is_superuser=True).
For the rest you can simply use a field for a normal User model to differentiate between subroles of a normal user.
class User(AbstractBaseUser):
can_participate_event = models.Boolean(default=False)
can_create_event = models.Boolean(default=False)
Or
class User(AbstractBaseUser):
permissions = models.CharField(default='') # and populate with e.g. 'create_event,participate_event'
Still you will need to check all those fields in your view probably. The more you add to your application, the hairier this becomes so I would suggest using a 3rd party library like rest-framework-roles (I'm the author) or guardian.
I'm trying to figure out the best way to set up the following django model (genericised for security reasons).
ThingA:
User(M2M through "UserRelation")
ThingB:
User(M2M through "UserRelation")
ThingC:
User(M2M through "UserRelation")
User:
Login_name
UserRelation:
User (foreginkey)
Thing (foreignkey) #is this generic to any of the above "things"
Privilege
I understand using "through" between two distinct models, but I'm not sure how to apply this to multiple models. Would I define a foreignkey for each of the "Thing" models in my UserRelation Model?
It looks like you are trying to setup a generic many-to-many relationship. There is a dedicated django app that you can be use for this purpose: django-gm2m
Here is how to use it in your generic case:
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from gm2m import GM2MField
class ThingA(models.Model):
pass
class ThingB(models.Model):
pass
class ThingC(models.Model):
pass
class User(models.Model):
login_name = models.CharField(max_length=255)
things = GM2MField(through='UserRelation')
class UserRelation(models.Model):
user = models.ForeignKey(User)
thing = GenericForeignKey(ct_field='thing_ct', fk_field='thing_fk')
thing_ct = models.ForeignKey(ContentType)
thing_fk = models.CharField(max_length=255)
privilege = models.CharField(max_length=1)
You can now access all the things for a given user and all the User instances for a given 'thing', as well as the privilege attribute for each UserRelation instance.
This will additionally provide you with a handful of benefits (reverse relations, prefetching, etc.) you may need. A GM2MField basically behaves exactly like a django ManyToManyField.
Disclaimer: I am the author of django-gm2m
Reading the Django Documentation:
get_user_model()
Instead of referring to User directly, you should reference the user
model using django.contrib.auth.get_user_model(). This method will
return the currently active User model – the custom User model if one
is specified, or User otherwise.
When you define a foreign key or many-to-many relations to the User
model, you should specify the custom model using the AUTH_USER_MODEL
setting.
I'm confused with the above text. Should I be doing this:
author = models.ForeignKey(settings.AUTH_USER_MODEL)
or this...
author = models.ForeignKey(get_user_model())
Both seem to work.
Using settings.AUTH_USER_MODEL will delay the retrieval of the actual model class until all apps are loaded. get_user_model will attempt to retrieve the model class at the moment your app is imported the first time.
get_user_model cannot guarantee that the User model is already loaded into the app cache. It might work in your specific setup, but it is a hit-and-miss scenario. If you change some settings (e.g. the order of INSTALLED_APPS) it might very well break the import and you will have to spend additional time debugging.
settings.AUTH_USER_MODEL will pass a string as the foreign key model, and if the retrieval of the model class fails at the time this foreign key is imported, the retrieval will be delayed until all model classes are loaded into the cache.
New since Django 1.11.
Since Django 1.11 you can use get_user_model() in both cases! So if you don't want to bother about it further, just take it.
"in both cases" means: if you need the user model for accessing its attributes, as well as if you want to define a ForeignKey/ManyToMany-relation.
From the changelog:
get_user_model() can now be called at import time, even in modules that define models.
so... is there still a reason to use settings.AUTH_USER_MODEL? Well, the docs still recommend the settings.AUTH_USER_MODEL (which is a string) for defining relations, but without giving an explicit reason. Might be beneficial for performance, but doesn't seem to matter much.
Code example:
from django.db import models
from django.contrib.auth import get_user_model
...
...
user = models.ForeignKey(
get_user_model(),
null=True, # explicitly set null, since it's required in django 2.x. - otherwise migrations will be incompatible later!
...
)
Since Django 1.11, get_user_model() actually uses settings.AUTH_USER_MODEL:
def get_user_model():
"""
Return the User model that is active in this project.
"""
try:
return django_apps.get_model(settings.AUTH_USER_MODEL, require_ready=False)
except ValueError:
raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'")
except LookupError:
raise ImproperlyConfigured(
"AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL
)
settings.AUTH_USER_MODEL returns a string (the location of the User model) e.g. user_accounts.User.
get_user_model() returns the ACTUAL model class, not a string.
So in cases where you need the User model, use get_user_model(). If you need it's location (module.model as a string), use the settings.AUTH_USER_MODEL.
as i read from book "Two Scoops of Django 3.x"
Using get_user_model():
This is bad, as it tends to create import loops.
bad example:
owner = models.OneToOneField(get_user_model()) -> BAD DONT DO IT
A way to fallback to the default user model if AUTH_USER_MODEL is not set:
from django.conf import settings
from django.contrib.auth.models import User
USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', User)
I'm trying to make sure that the first name and last name field are not optional for the auth User model but I'm not sure how to change it. I can't use a sub class as I have to use the authentication system.
Two solutions I can think of are:
to put the name in the user profile but it's a little silly to have a field that I can't use correctly.
To validate in the form rather than in the model. I don't think this really fits with Django's philosophy...
For some reason I can't seem to find a way to do this online so any help is appreciated. I would have thought that this would be a popular question.
Cheers,
Durand
Simplest solution
Just create a custom UserRegisterForm which inherits the django's default UserCreationForm.
The first_name and last_name are already attributes of django's default User. If you want to make them as required fields, then recreate those fields as forms.CharField(...).
Now use your own User register form.
# Contents usersapp/forms.py
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
# Inherit Django's default UserCreationForm
class UserRegisterForm(UserCreationForm):
first_name = forms.CharField(max_length=50) # Required
last_name = forms.CharField(max_length=50) # Required
# All fields you re-define here will become required fields in the form
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name', 'password1', 'password2']
I would definitely go with validating on the form. You could even go as far as having more form validation in the admin if you felt like it.
Thanks Mbuso for the advice. Here's my full implementation for those who are interested. Before taking a look at the source, let's see what it looks like:
I've implemented a profile model, but this will work just fine without it.
from django.core.exceptions import ValidationError
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import User
from apps.profiles.models import Profile
# Define an inline admin descriptor for Profile model
# which acts a bit like a singleton
class UserProfileInline(admin.StackedInline):
model = Profile
can_delete = False
verbose_name_plural = 'profile'
class MyUserChangeForm(UserChangeForm):
def clean_first_name(self):
if self.cleaned_data["first_name"].strip() == '':
raise ValidationError("First name is required.")
return self.cleaned_data["first_name"]
def clean_last_name(self):
if self.cleaned_data["last_name"].strip() == '':
raise ValidationError("Last name is required.")
return self.cleaned_data["last_name"]
# Define a new User admin
class MyUserAdmin(UserAdmin):
form = MyUserChangeForm
inlines = UserProfileInline,
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
Note: If you do implement a profile model, recommend using UserProfile as the name, since is this is what's in the documentation and seems to be the standard (this part was developed before I started working on the project). If you're using Django 1.5 or higher, skip UserProfile all together and extend the User model.
The Django way of extending the basic User model is through user profiles: see "Storing additional information about users".
If it does not fit your needs, django.contrib.auth is just a Django application, I would simply fork it. As long as you abide by the original interface, I think you will be out of trouble.
Another option is Pinax - it has OpenId support built in, you can use it with your own openid provider. OpenId native support is a battery I really miss in Django.
in django by default when syncdb is run with django.contrib.auth installed, it creates default permissions on each model... like foo.can_change , foo.can_delete and foo.can_add. To add custom permissions to models one can add class Meta: under the model and define permissions there, as explained here https://docs.djangoproject.com/en/4.1/topics/auth/customizing/#custom-permissions
My question is that what should I do if I want to add a custom permission to the User model? like foo.can_view. I could do this with the following snippet,
ct = ContentType.objects.get(app_label='auth', model='user')
perm = Permission.objects.create(codename='can_view', name='Can View Users',
content_type=ct)
perm.save()
But I want something that plays nicely with syncdb, for example the class Meta under my custom models. Should I just have these in class Meta: under UserProfile since that is the way to extend the user model. but is that the RIGHT way to do it? Wouldn't that tie it to UserProfile model?
You could do something like this:
in the __init__.py of your Django app add:
from django.db.models.signals import post_syncdb
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import models as auth_models
from django.contrib.auth.models import Permission
# custom user related permissions
def add_user_permissions(sender, **kwargs):
ct = ContentType.objects.get(app_label='auth', model='user')
perm, created = Permission.objects.get_or_create(codename='can_view', name='Can View Users', content_type=ct)
post_syncdb.connect(add_user_permissions, sender=auth_models)
I don't think there is a "right" answer here, but i used the exact same code as you except i changed Permission.objects.create to Permission.objects.get_or_create and that worked find to sync with syncdb
An updated answer for Django 1.8. The signal pre_migrate is used instead of pre_syncdb, since syncdb is deprecated and the docs recommend using pre_migrate instead of post_migrate if the signal will alter the database. Also, #receiver is used to connect add_user_permissions to the signal.
from django.db.models.signals import pre_migrate
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import models as auth_models
from django.contrib.auth.models import Permission
from django.conf import settings
from django.dispatch import receiver
# custom user related permissions
#receiver(pre_migrate, sender=auth_models)
def add_user_permissions(sender, **kwargs):
content_type = ContentType.objects.get_for_model(settings.AUTH_USER_MODEL)
Permission.objects.get_or_create(codename='view_user', name='View user', content_type=content_type)
This is a bit hacky but mentioning it here anyway for reference.
My site has a generic model called Setting, which stores various settings concerning the site I want certain users to be able to edit, without needing to go through me the developer (like registration limit, or an address, or the cost of items, etc).
All the permissions that don't nicely map onto other models (eg "Send Password Reminder Email to Student", "Generate Payment Reconciliation Report", "Generate PDF Receipt"), which really just relate to pages that get viewed in the admin area, get dumped onto this Setting model.
For example, here's the model:
class Setting(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(editable=False)
description = models.TextField()
value = models.TextField()
class Meta:
#for permissions that don't really relate to a particular model, and I don't want to programmatically create them.
permissions = (
("password_reminder", "Send Password Reminder"),
("generate_payment_reconciliation_report", "Generate Payment Reconciliation Report"),
("generate_pdf_receipt", "Generate PDF Receipt"),
)
Do each of those settings strictly relate to the Setting model? No, which is why I said this is a bit hacky. But it is nice that I can now just dump all those permissions here, and Django's migration commands will take care of the rest.