Django Admin- Limiting ForeignKey Field's choices - python

I've been working on my Django Project.
I use custom Forms (a class called Form), and I've got a class called Restricted_Form. The point of this class is that an Admin user ("staff status" user") has the option restrict which users or groups can have access to the Forms/submit filled Forms:
#....models.py:
from django.contrib.auth.models import User, Group
class Restricted_Form(models.Model):
Form = models.ForeignKey('forms.Form')
Authorized_Users = models.ManyToManyField(User, blank=True)
Authorized_Groups = models.ManyToManyField(Group, blank=True)
user = models.ForeignKey(User, blank=True, null=True, related_name="restriction_owner")
"Form" itself has:
user = models.ForeignKey(User, blank=True, null=True)
#this is always the user who created it
My issue is limiting what the non-superuser admins can do. They should only be able to create Restricted_Form objects including a Form they have created themselves. In practice, when they create a Restricted_Form, only "Forms" that they have created themselves should appear as options to select, in the drop-down menu.
This is my related admin class right now:
#...admin.py
class RestrictedFormAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ("Form", "Authorized_Users", "Authorized_Groups")}),]
def save_model(self, request, obj, form, change):
if getattr(obj, 'user', None) is None:
obj.user = request.user
obj.save()
def get_queryset(self, request):
qs = super(RestrictedFormAdmin, self).get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(user=request.user)
def has_change_permission(self, request, obj=None):
if not obj:
return True
return obj.user == request.user or request.user.is_superuser
admin.site.register(Restricted_Form, RestrictedFormAdmin)
Help will be greatly appreciated.

You can try something like this:
class RestrictedFormAdmin(admin.ModelAdmin):
def render_change_form(self, request, context, *args, **kwargs):
context['adminform'].form.fields['Form'].queryset = Form.objects.filter(user=request.user)
return super(RestrictedFormAdmin, self).render_change_form(request, context, args, kwargs)

Related

(Django) How to create a verification mixin that references two models?

I have two classes Organisation, and Staff in addition to the User class. I want to create a mixin called "UserIsAdminMixin" that checks whether the user logged in has the role "admin" in a specific Organisation.
The classes (simplified)
class Organisation(models.Model):
name = models.CharField(max_length=50)
class Staff(models.Model):
class Role(models.TextChoices):
ADMIN = 'ADMIN', "Admin"
STAFF = 'STAFF', "Staff"
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, db_index=True, related_name="staff_profiles", on_delete=models.SET_NULL)
organisation = models.ForeignKey('organisations.Organisation', db_index=True, related_name="staff", on_delete=models.CASCADE)
role = models.CharField(max_length=10, choices=Role.choices, default=Role.STAFF)
I then currently have this for my UserIsAdmin mixin:
class UserIsAdminMixin:
def dispatch(self, request, *args, **kwargs):
if self.request.user.staff_profiles.filter(organisation=self.get_object(), role=Staff.Role.ADMIN):
return super().dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
This works great for this view:
organisations.views.py (URL: organisation/<int:pk>)
class OrganisationDetail(LoginRequiredMixin, UserIsAdminMixin, generic.DetailView):
model = Organisation
template_name= "organisations/detail.html"
login_url = "login"
But I'd also like it to work for this view as well, which it obviously doesn't as self.get_object() returns a Staff object in this case when it's expecting an Organisation object:
staff.views.py (URL: organisation/<int:pk_alt>/staff/<int:pk>)
class StaffDetail(LoginRequiredMixin, UserIsAdminMixin, generic.DetailView):
model = Staff
template_name="staff/detail.html"
login_url = "login"
I was able to make changes to the mixin to get it to work in the second scenario but not the first:
class UserIsAdminMixin:
def dispatch(self, request, *args, **kwargs):
if self.request.user.staff_profiles.filter(organisation__pk=self.kwargs['pk_alt']), role=Staff.Role.ADMIN):
return super().dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
So is there any way I can change the mixin so it works for both Organisation and Staff models to check that the User is a staff member with the role of admin for a given organisation?
Any help would be greatly appreciated!
Many Thanks,
GoingRoundInCircles
Well, a possible solution is to have a get_organisation_id() method in both your views.
Your mixin:
class UserIsAdminMixin:
def dispatch(self, request, *args, **kwargs):
organisation_id = self.get_organisation_id()
if self.request.user.staff_profiles.filter(organisation_pk=organisation_id, role=Staff.Role.ADMIN):
return super().dispatch(request, *args, **kwargs)
else:
raise PermissionDenied
StaffDetail:
class StaffDetail(LoginRequiredMixin, UserIsAdminMixin, generic.DetailView):
model = Staff
template_name="staff/detail.html"
login_url = "login"
def get_organisation_id(self):
return self.kwargs.get('pk_alt')
OrganisationDetail:
class OrganisationDetail(LoginRequiredMixin, UserIsAdminMixin, generic.DetailView):
model = Organisation
template_name= "organisations/detail.html"
login_url = "login"
def get_organisation_id(self):
return self.get_object().id

Inline formset causing error: 'NoneType' object has no attribute 'is_ajax'

I am using bootstrap-modal-forms to show a user a formset with some inline forms. It is possible for the user to save the form if data is only entered into the original form, but if the inline formset has data then I get the following error:
'NoneType' object has no attribute 'is_ajax'
The inline formset was working correctly before I tried to implement them in the modal form. The problem seems to arise only when the inline formset (projectimages) is saved it is a NoneType.
My views.py
class ProjectCreate(BSModalCreateView):
form_class = ProjectForm
template_name = 'project_form.html'
success_message = 'Success: %(project_name)s was created.'
def get_success_url(self):
return reverse_lazy('project-detail', kwargs={'project': self.object.slug})
def get_context_data(self, **kwargs):
data = super(ProjectCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['projectimages'] = ProjectFormSet(self.request.POST, self.request.FILES,)
else:
data['projectimages'] = ProjectFormSet()
return data
def form_valid(self, form):
form.instance.date_created = timezone.now()
context = self.get_context_data()
projectimages = context['projectimages']
with transaction.atomic():
self.object = form.save()
if projectimages.is_valid():
projectimages.instance = self.object
projectimages.save()
return super(ProjectCreate, self).form_valid(form)
My forms.py
class ProjectForm(BSModalForm):
class Meta:
model = Project
exclude = ['date_created', 'slug']
ProjectFormSet = inlineformset_factory(
Project,
ProjectImage,
can_delete=True,
form=ProjectForm,
extra=1,
)
My models.py
class Project(models.Model):
project_name = models.CharField(max_length=100)
date_created = models.DateTimeField('Created on')
slug = models.SlugField(unique=True)
def __str__(self):
return self.project_name
def save(self, *args, **kwargs):
self.slug = slugify(str(self))
super(Project, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('project-list')
class ProjectImage(models.Model):
image = models.ImageField(verbose_name='Additional Images', upload_to=project_directory_path)
project = models.ForeignKey(Project, on_delete=models.PROTECT)
annotation = models.CharField(max_length=200, blank=True)
I expect the user to be able to add as many images to the modal formset as they like.
The BSModalForm expects you to initialise it with the request. This happens in your BSModalCreateView for the main form but not for your formset, because you initialise it manually.
So when initialising, just add the form_kwargs attribute:
if self.request.POST:
data['projectimages'] = ProjectFormSet(
self.request.POST, self.request.FILES,
form_kwargs={'request': self.request})
else:
data['projectimages'] = ProjectFormSet(form_kwargs={'request': self.request})
Note that I think the form you set in ProjectFormSet is wrong, because it should be a form for a ProjectImage model, not a Project. It should actually be called ProjectImageFormSet to better reflect what it is.
You probably want to remove form=ProjectForm as it probably doesn't need to be a BSModalForm (not sure about that). In that case you should not pass the request in form_kwargs. If not, you just need to create another ProjectImageForm class.
Finally, you should not return super().form_valid() because that will save the main form a second time (you already did). Do the redirect yourself.

How to write the permission of a instance's user is the request.user?

How can I write the IsThePhysicalServerUser?
you see my UpdateAPIView:
class PhysicalServerCustomerUpdateAPIView(UpdateAPIView):
"""
Customer's update PhysicalServer
"""
serializer_class = CustomerUpdatePhysicalServerSerializer
permission_classes = [IsThePhysicalServerUser]
queryset = PhysicalServer.objects.all()
There I want to write a permission which is IsThePhysicalServerUser.
the PhysicalServer model is bellow:
class PhysicalServer(models.Model):
"""
实体服务器
"""
name = models.CharField(max_length=32)
desc = models.CharField(max_length=256, null=True, blank=True)
...
user = models.ForeignKey(to=User, related_name="physical_servers", null=True, blank=True, on_delete=models.SET_NULL)
I want the IsThePhysicalServerUser permission realize the PhysicalServer instance's use is the request user. How to write the permission?
Just check the instance's user equals request.user:
class IsSuperAdminOrPhysicalServerCustomer(permissions.BasePermission):
"""
SuperUser or instance.user equals request.user
"""
def has_object_permission(self, request, view, obj):
# if you want everyone can see user info
if request.method in permissions.SAFE_METHODS:
return True
# if you use Django2.0 is_authenticated(request.user) should be changed to request.user.is_authenticated
if request.user and request.user.is_authenticated:
# instance.user equal self or is superuser
return obj.user == request.user or request.user.is_superuser
else:
return False

Change serializers on per-object basis within one ViewSet?

I'm working on a project with some social features and need to make it so that a User can see all details of his profile, but only public parts of others' profiles.
Is there a way to do this within one ViewSet?
Here's a sample of my model:
class Profile(TimestampedModel):
user = models.OneToOneField(User)
nickname = models.CharField(max_length=255)
sex = models.CharField(
max_length=1, default='M',
choices=(('M', 'Male'), ('F', 'Female')))
birthday = models.DateField(blank=True, null=True)
For this model, I'd like the birthday, for example, to stay private.
In the actual model there's about a dozen such fields.
My serializers:
class FullProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
class BasicProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = read_only_fields = ('nickname', 'sex', 'birthday')
A custom permission I wrote:
class ProfilePermission(permissions.BasePermission):
"""
Handles permissions for users. The basic rules are
- owner and staff may do anything
- others can only GET
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return request.user == obj.user or request.user.is_staff
And my viewset:
class RUViewSet(
mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet with update/retrieve powers."""
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
What I'd like is for request.user's own profile in the queryset to be serialized using FullProfileSerializer, but the rest using BasicProfileSerializer.
Is this at all possible using DRF's API?
We can override the retrieve() and list methods in our ProfileViewSet to return different serialized data depending on the user being viewed.
In the list method, we serialize all the user instances excluding the current user with the serializer returned from get_serializer_class() method. Then we serialize the current user profile information using the FullProfileSerializer explicitly and add this serialized data to the data returned before.
In the retrieve method, we set a accessed_profile attribute on the view to know about the user the view is displaying. Then, we will use this attribute to decide the serializer in the get_serializer_class() method.
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def list(self, request, *args, **kwargs):
instance = self.filter_queryset(self.get_queryset()).exclude(user=self.request.user)
page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
serializer = self.get_serializer(instance, many=True)
other_profiles_data = serializer.data # serialized profiles data for users other than current user
current_user_profile = <get_the_current_user_profile_object>
current_user_profile_data = FullProfileSerializer(current_user_profile).data
all_profiles_data = other_profiles_data.append(current_user_profile_data)
return Response(all_profiles_data)
def retrieve(self, request, *args, **kwargs):
self.accessed_profile = self.get_object() # set this as on attribute on the view
serializer = self.get_serializer(self.accessed_profile)
return Response(serializer.data)
def get_serializer_class(self):
current_user = self.request.user
if current_user.is_staff or (self.action=='retrieve' and self.accessed_profile.user==current_user):
return FullProfileSerializer
return BasicProfileSerializer
I managed to hack together the solution that provides the wanted behaviour for the detail view:
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
def get_serializer(self, instance=None, *args, **kwargs):
if hasattr(instance, 'user'):
user = self.request.user
if instance.user == user or user.is_staff:
kwargs['instance'] = instance
kwargs['context'] = self.get_serializer_context()
return FullProfileSerializer(*args, **kwargs)
return super(ProfileViewSet, self).get_serializer(
instance, *args, **kwargs)
This doesn't work for the list view, however, as that one provides the get_serializer method with a Django Queryset object in place of an actual instance.
I'd still like to see this behaviour in a list view, i.e. when serializing many objects, so if anyone knows a more elegant way to do this that also covers the list view I'd much appreciate your answer.

How to assign the User object to save method in Django

I am trying to log the activities during save operation to track all the changes to user model. my approach is as follows.
class User(AbstractUser):
undergrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
undergrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
postgrad_college = models.CharField(max_length=20, choices=COLLEGE_CHOICES)
postgrad_degree = models.CharField(max_length=20, choices=COLLEGE_DEGREES)
currently_working_on = models.TextField()
previous_work_experience = models.TextField()
previous_internship_experience = models.TextField()
def __str__(self):
return self.username
def save(self, *args, **kwargs):
Log(user=User, actions="Updated profile",
extra={"undergrad_college": self.undergrad_college,
"undergrad_degree": self.undergrad_degree,
"postgrad_college": self.postgrad_college,
"postgrad_degree": self.postgrad_degree,
"currently_working_on": self.currently_working_on,
"previous_work_experience": self.previous_work_experience,
"previous_internship_experience": self.previous_internship_experience
})
super(User, self).save(args, **kwargs)
my views are like this for handling the logging.
class ActivityMixin(LoginRequiredMixin):
def get_context_data(self, **kwargs):
context = super(ActivityMixin, self).get_context_data(**kwargs)
context['activities'] = Log.objects.filter(user=self.request.user)
return context
class IndexListView(ActivityMixin, ListView):
template_name = 'pages/home.html'
model = User
I get this error while performing the update action.
Cannot assign "<class 'users.models.User'>": "Log.user" must be a "User" instance.
Update view is as follows
class UserUpdateView(LoginRequiredMixin, UpdateView):
form_class = UserForm
# we already imported User in the view code above, remember?
model = User
# send the user back to their own page after a successful update
def get_success_url(self):
return reverse("users:detail",
kwargs={"username": self.request.user.username})
def get_object(self, **kwargs):
# Only get the User record for the user making the request
return User.objects.get(username=self.request.user.username)
How to assign the User model instance to the Log function. I cant get this working. I am Django newbie.
Looks like pretty straightforward, replace User with self:
Log(user=User, ...
Log(user=self, ...

Categories

Resources