I have started a project with an idea that's more than I know I can handle but hey... some of us learn the dumb way right?
Currently have 2 models: User
model that uses the AbstractUser
class and then I have a Role
model that provides many roles that may change over time so I need it to be dynamic.
This is what my api/models.py
looks like:
from django.db import models
from django.contrib.auth.models import AbstractUser
from django import forms
class Role(models.Model):
'''
The Role entries are managed by the system,
automatically created via a Django data migration.
'''
CABLER = 1
CLIENT = 2
ENGINEER = 3
PROJECTMANAGER = 4
SALES = 5
PARTNER = 6
ADMIN = 6
ROLE_CHOICES = (
(CABLER, 'cabler'),
(CLIENT, 'client'),
(ENGINEER, 'engineer'),
(PROJECTMANAGER, 'project manager'),
(SALES, 'sales'),
(PARTNER, 'partner'),
(ADMIN, 'admin'),
)
id = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, primary_key=True)
def __str__(self):
return self.get_id_display()
class User(AbstractUser):
roles = models.ManyToManyField(Role, related_name='roles')
I'm running into several problems but currently when I use postman or The DRF API views I can't create a user with roles or without roles.
Attempting with roles:
Direct assignment to the forward side of a many-to-many set is prohibited. Use roles.set() instead.
Attempting without roles:
{
"roles": [
"This list may not be empty."
]
}
Now, I know I have to first create the User and then assign the role, however, I cannot seem to find a way to do it through the API.
Here's my UserSerializer
class:
class UserSerializer(serializers.ModelSerializer):
# roles = serializers.CharField(source='get_roles_display', )
# roles = RoleSerializer(many=True, read_only=False, )
class Meta:
model = User
fields = ('url', 'username', 'password', 'email', 'roles')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
password = validated_data.pop('password')
instance = User(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
I have seen recommendations to create a form.py
file and add a save method but I'm not certain that it applies as this will be strictly an API as my backend and will be implementing AngularJS for my front end.
Also any time I try to save or update the user under the serializer User.roles.id
returns a User does not have an attribute named roles...
So I'm hoping someone may be able to help. Thanks.
First and unrelated to your question , your roles m2m field should be updated to:
roles = models.ManyToManyField(Role, related_name='users')
The related_name is how you reference the current model (User) from the model passed into ManyToManyField (Role). With my suggestion you'd write Role.objects.first().users
rather than Role.objects.first().roles
.
Secondly, if it's possible I'd recommend passing the roles to create/update a user as a list of primary keys. Even if this means creating a second serializer property that's only for writes. For example:
class UserSerializer(serializers.ModelSerializer):
roles = RoleSerializer(many=True, read_only=True)
role_ids = serializers.PrimaryKeyRelatedField(
queryset=Role.objects.all(),
many=True, write_only=True)
class Meta:
model = User
fields = ('url', 'username', 'password', 'email', 'roles', 'role_ids')
extra_kwargs = {'password': {'write_only': True}}
def create(self, validated_data):
password = validated_data.pop('password')
# Allow the serializer to handle the creation. Don't do it yourself.
instance = super().create(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
When your API spits out users it should look like:
{
'url': '...',
'username': '...',
'email': '...',
'roles': [{'id': 1, 'name': 'client'}],
}
When you POST to it you should use:
{
'url': '...',
'username': '...',
'email': '...',
'password': '...',
'role_ids': [1, 2],
}
As an aside, the reason you were getting the error User does not have an attribute named roles.
is because you can't pass roles
into User
's constructor. User.roles
needs to be set after you have an instance. You should have done:
user = User(**validated_data)
user.roles.set(roles) # If you need to re-write the entire list.
Although off the top of my head, I'm not sure it would work. You might have to save it beforehand. That's why I suggest you use super().create
instead.