I have a model in my app called "portal", in portal/models.py:
from django.db import models
from django.contrib.auth.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save
from wagtail.snippets.models import register_snippet
from wagtail.admin.edit_handlers import FieldPanel
#register_snippet
class StaffRoles(models.Model):
role = models.CharField(max_length=154, unique=True, help_text="Create new staff roles here, these roles an be assigned to 'staff' users.")
panels = [
FieldPanel('role'),
]
def __str__(self):
return self.role
class Meta:
verbose_name = "Staff Role"
verbose_name_plural = "Staff Roles"
#register_snippet
class Staff(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=1024, blank=True)
roles = models.ManyToManyField(StaffRoles, blank=True)
def __str__(self):
return str(self.user.first_name) + " " + str(self.user.last_name)
class Meta:
verbose_name = "Staff"
verbose_name_plural = "Staff"
#receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created and instance.is_superuser:
Staff.objects.create(user=instance)
Any new superuser is automatically given the Staff model. Any existing (non superuser) user can also be added as Staff.
I want Staff members to be able to set a bio and a role. Roles can be added through the Snippets page in Wagtail admin.
Right now, Staff is registered as a snippet, this means any Staff member can edit another Staff member's Staff attributes.
I want to customise the Wagtail User Account Settings by adding a form to each user's respective Staff attributes. This way, I can lock out the Staff snippet so each user who has a Staff object linked to their User model can change only their own Staff fields/attributes.
Following the Wagtail documentation from here, I first created the file portal/forms.py with the content:
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from wagtail.users.models import UserProfile
from .models import Staff
class CustomStaffSettingsForm(forms.ModelForm):
class Meta:
model = Staff
exclude = []
And another file portal/wagtail_hooks.py with the contents:
from wagtail.admin.views.account import BaseSettingsPanel
from wagtail.core import hooks
from .forms import CustomStaffSettingsForm
#hooks.register('register_account_settings_panel')
class CustomStaffSettingsPanel(BaseSettingsPanel):
name = 'custom_staff'
title = "Staff settings"
order = 500
form_class = CustomStaffSettingsForm
form_object = 'profile'
This doesn't work as intended. When accessing "Account settings" and scrolling down to the "Staff settings" I can see the CustomStaffSettingsForm as intended. Although the correct user is pre-selected in the User field, I can still select other users. Also, the stored values for bio and roles aren't being retrieved and new values aren't being saved.
Below are the Staff Roles registered in the snippets page:
Below is the Staff model for the user "sid" in the snippets page:
Below is what the Staff settings section in the "Account settings" looks like.
As you can see, although the User dropdown is selected to the right user, I can still click and select another user. The bio and roles fields aren't filled with the correct values from the snippets page and not shown in the picture, filling and saving values doesn't work.
I hope my question is as detailed as possible, how do I fix this?
After a few hours of digging, I managed to answer my own question.
A related_name needs to be added with the value of profile to Staff.user. This prevents the 'User' object has no attribute 'profile' error message
class Staff(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
...
We need to exclude the user field from the CustomStaffSettingsForm. This is to prevent users to select a different user and change another user's Staff fields.
class CustomStaffSettingsForm(forms.ModelForm):
class Meta:
model = Staff
exclude = ["user"]
The tricky part (at least for me) was to render the correct instance of the form, in other words, if a user named "sid" is accessing this form, they need to be able to see their own Staff fields which are stored and also be able to change the stored values. To achieve this, I had to override the get_form method which CustomStaffSettingsPanel inherited form Django's BaseSettingsPanel. It's easier for me to paste the entire wagtail_hooks.py contents here below.
from wagtail.admin.views.account import BaseSettingsPanel
from wagtail.core import hooks
from .forms import CustomStaffSettingsForm
#hooks.register('register_account_settings_panel')
class CustomStaffSettingsPanel(BaseSettingsPanel):
name = 'custom_staff'
title = "Staff settings"
order = 300
form_class = CustomStaffSettingsForm
form_object = 'profile'
def get_form(self):
"""
Returns an initialised form.
"""
kwargs = {
'instance': self.request.user.profile,
'prefix': self.name
}
if self.request.method == 'POST':
return self.form_class(self.request.POST, **kwargs)
else:
return self.form_class(**kwargs)
The key part of the code above is 'instance': self.request.user.profile. Passing this argument into self.form_class results in the correct user's stored Staff fields and values to be retrieved and stored correctly.
This solution allows me to make sure superusers who are also registered as Staff can edit their own Staff fields and have access to others, this is a functionality which cannot be achieved easily with Snippets.
As Staff is no longer registered as a Snippet, we lose access to change another user's Staff fields. This can be fixed by adding the following in portal/admin.py:
admin.site.register(Staff)
Only superusers who had been created via python manage.py createsuperuser or has had staff_status set to true can access the Django Admin interface at http://localhost:8000/django-admin. Unlike Snippets, this way can be used to allow only the Django superuser staff to access and modify Staff fields.
What about non-superusers who are registered as Staff? Non-superusers cannot access the Wagtail admin, therefore they cannot access the Account Settings page. In most cases, this shouldn't be a problem as the ideal use-case is where only the superusers can be set as Staff. If you want to set non-superusers as Staff you might have to create your own views and forms so the user can change their Staff fields outside of Wagtail admin.
Related
I am building a BlogApp AND i made a feature of Favorite Users in the ManyToManyField.
It means user_1 can add multiple favorite users in one field. I build a view to add users.
BUT when i create an instance to store favorite users from Admin before adding favorite users from site AND then if i add favorite users from site then they are adding. BUT if there is not already a instance of request.user's FavouriteUsers in Admin then it is not adding and creating a new object.
So, A new object for storing favorite users of request.user is not adding, AND if object is already there then they are adding.
models.py
class FavoriteUsers(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE)
favorite_users = models.ManyToManyField(User, related_name='favorite_users', blank=True)
views.py
def AddFavUsers(request,user_id):
obj = FavoriteUsers.objects.filter(user=request.user)
user = get_object_or_404(User,id=user_id)
for ob in obj:
ob.favorite_users.add(user)
ob.user.add(request.user)
ob.save()
return redirect('home')
Image of already created object
When object is already , manually created then favorite users are adding BUT if it is not then new object is not creating.
Any help would be much Appreciated.
Thank You in Advance.
Your view is basically adding the user to all FavoriteUsers that are linked to the User. But it is not said at all that there are such FavoriteUsers.
We thus can construct a FavroiteObject with:
def AddFavUsers(request,user_id):
obj = FavoriteUsers.objects.create(
user=request.user
)
obj.favorite_users.add(user_id)
# …
the modeling is also a bit odd, since we here create a third table, it makes more sense to construct a model Favorite with two ForeignKeys that thus acts as a junction table. So this should look like:
from django.conf import settings
class Favorite(models.Model):
user_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='favorites_from'
)
user_from = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name='favorites_to'
)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user_from', 'user_to'], name='favorite_only_once')
]
If you then want to make a user_id a favorite of the logged in user, you can work with:
from django.contrib.auth.decorators import login_required
#login_required
def AddFavUsers(request, user_id):
Favorite.objects.create(user_from=request.user, user_to_id=user_id)
# …
You can then obtain the favorite users of a user with:
User.objects.filter(favorites_from__user_from=my_user)
where my_user is the user from which you want to obtain the favorite users.
Note: You can limit views to a view to authenticated users with the
#login_required decorator [Django-doc].
The ManyToMany field already handles this. You can add a favorite_users property to your User model, which will point to the User model and Django will create the connecting table. Beware of the symmetrical parameter, if you leave it out it'll be a bidirectional relation.
class MyUser(models.Model):
...
favorite_users = models.ManyToManyField("MyUser", symmetrical=False, blank=True)
If you still want to write the connecting model yourself, check out the through parameter.
I'm working on website whose an app which has class called Members whose a field that is related to the builtin User class from django.contrib.auth.models and it looks like
class Members(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
member_image = models.ImageField(upload_to='unknown')
member_position = models.CharField(max_length=255)
...
So as you can see when I'm adding member_image as a user I have also to select the user which doesn't make sense to me because I want to detect which user is logged in and pass his/her id as default parameter
like
class Members(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, default=request.user.id)
and after remove the user field in the admin panel like
class MembersAdmin(admin.ModelAdmin):
fields = ('member_image', 'member_position', ...)
so that if the user field doesn't selected it will set the logged in user_id by default
but to access request out of the views.py is not possible.
so how will I achieve this I also tried the following answers
Access session / request information outside of views in Django
Accessing request.user outside views.py
Django: How can I get the logged user outside of view request?, etc
but still not get it
Modify MembersAdmin save_model method and attach request.user to the object prior to saving.
class MembersAdmin(admin.ModelAdmin):
fields = ('member_image', 'member_position', ...)
def save_model(self, request, obj, form, change):
obj.user = request.user
super().save_model(request, obj, form, change)
For exclude the current logged in User for particular page or view, You can try this :-
from django.contrib.auth import get_user_model
User = user_model()
def some_view(request):
exclude_current_user = User.objects.exclude(user=request.user)
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 am extending the Django User model to include a foreign key pointing at another model like so (just like it says in the Django docs):
models.py:
class Ward(models.Model):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
# Extending the user model
class WardMember(models.Model):
user = models.OneToOneField(User)
ward = models.ForeignKey(Ward)
def __unicode__(self):
return self.ward.name
admin.py:
class WardMemberInline(admin.StackedInline):
model = WardMember
can_delete = False
verbose_name_plural = 'ward member'
# Define a new User admin
class UserAdmin(UserAdmin):
inlines = (WardMemberInline, )
admin.site.register(Ward)
# Re-register UserAdmin to get WardMember customizations
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
When I create a new user in the admin interface I want this new WardMember.ward extension to be required. Currently it's not enforcing that. Here's what happens:
Create user succeeds without a ward
Create other records as user succeed
Edit user now won't let me save unless there is a ward selected
I'd really like #1 above to fail.
I've tried figuring out how to override save() for User using a proxy object but that's not working. I looked into the pre_save signal but the docs explicitly say that's not for vetoing saves.
What is the right approach?
Additional information:
I'm using 1.4. I see that in 1.5 I can extend the user class but I'm not in a position to update to 1.5 just yet.
I ended up forging ahead with Django 1.5, but I'll leave this here in case someone has a final answer to contribute that works with 1.4.
In django 1.3.1 I use this code and works fine:
from django.contrib.auth.models import User
class FilterSearchQueries(models.Model):
title = models.CharField(max_length=250)
owner = models.ForeignKey(User)
place = models.CharField(max_length=250)
query = models.TextField()
Let's say you have a django model with a OneToOne / Unique ForeignKey relationship with a User, as show on the Django documentation on how to create a UserProfile.:
Now let's say you have a view method that takes a request you can get a user from. What is the best way to query for the profile associated with that user?
from django.contrib.auth.models import User
# sample user profile model associated with user
class UserProfile(models.Model):
likes_spam = models.BooleanField()
user = models.OneToOneField(User)
#view method
def forward_to_practice_home(request):
user = request.user
profile_for_user = #insert code here that would get the profile for that user
related_names are very helpful. If you change your user profile definition to:
class UserProfile(models.Model):
likes_spam = models.BooleanField()
user = models.OneToOneField(User, related_name='profile')
then you can use profile as follows:
def forward_to_practice_home(request):
user = request.user
profile_for_user = user.profile
UserProfile.objects.get(user=user)
You may use a special method called get_profile()
profile_for_user = user.get_profile()
Be reminded that you have to set the AUTH_PROFILE_MODULE in the settings.py
However, this is deprecated in Django 1.5 because it adds the support of user model customization