Django - object level premission and class based generic views - python

This is the model:
class Car(models.Model):
user = models.ForeignKey(User, related_name='cars')
name = models.CharField(max_length=64)
Url pattern goes something like this:
url(r'^car/(?P<pk>\d+)/$', login_required(CarDetails.as_view()), name='car_details)
And view:
class CarDetail(DetailView):
context_object_name = 'car'
template_name = 'my_app/car_details.html'
model = models.Car
def get_object(self, *args, **kwargs):
car = super(CarDetail, self).get_object(*args, **kwargs)
if car.user != self.request.user:
raise PermissionDenied()
else:
return car
This works fine, but in every class I have to override get_object to prevent user to mess with someone else's objects. This includes editing and deleting for every model I have and this is serious violation of DRY principle.
Is there a better way to do this? Something like login_required decorator maybe?

What about definig base class (or mixin) and using inheritance?
class CurUserOnlyDetailView(DetailView):
def get_object(self, *args, **kwargs):
obj = super(CurUserOnlyDetailView, self).get_object(*args, **kwargs)
if obj.user != self.request.user:
raise PermissionDenied()
else:
return obj
class CarDetail(CurUserOnlyDetailView):
context_object_name = 'car'
template_name = 'my_app/car_details.html'
model = models.Car
# another view, no DRY violation
class BikeDetail(CurUserOnlyDetailView):
context_object_name = 'bike'
template_name = 'my_app/bike_details.html'
model = models.Bike

The solution was more-or-less simple as DrTyrsa proposed in his answer, with one little difference. I created base class CurUserOnly that inherits object, instead of DetailView (I wanted to use this class with DeleteView and UpdateView, too) and now CarDetail inherits CurUserOnly and DetailView, CarDelete inherits CurUserOnly and DeleteView and so on...
Funny thing is that I tried this before, but it didn't work because I forgot python's MRO and DetailView was first in inheritance list when CurUserOnly should be!
In the end, here is CurUserOnly class:
class CurUserOnly(object):
def get_object(self, *args, **kwargs):
obj = super(CurUserOnly, self).get_object(*args, **kwargs)
user_attribute = getattr(self, 'user_attribute', 'user')
user = obj
for part in user_attribute.split('.'):
user = getattr(user, part, None)
if user != self.request.user:
raise PermissionDenied()
else:
return obj
And if I have a model that does not have direct contact to user all I need to do is add user_attribute field. For example, if I have model Tyre with ForeignKey to Car its DeleteView would look like this:
class TyreDelete(CurUserOnly, DeleteView):
model = models.Tyre
user_attribute = 'car.user'
This answer was posted as an edit to the question Django - object level premission and class based generic views by the OP del-boy under CC BY-SA 3.0.

Related

Calling ListView Class from UpdateView class in Django

I am trying to call DealerEmployeeView class from DealerUpdateView Class. Problem is my DealerUpdateView class does not work without mentioning ModelFormMixin in class chain inheritance. But when I mention DealerEmployeeView after ModelFormMixin it cause an error:
TypeError: Cannot create a consistent method resolution
order (MRO) for bases object, TemplateResponseMixin, ModelFormMixin, DealerEmployeeView
Is there way to solve this class inheritance problem without creating new class view?
DealerUpdateView
class DealerUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView, ModelFormMixin, DealerEmployeeView):
permission_required = ''
model = Dealer
fields = ['dealer_name', 'contact_name', 'contact_surname', 'partita_iva', 'email', 'phone_number', 'description']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
DealerEmployeeView
class DealerEmployeeView(ListView):
order_type = 'asc'
def get(self, request, *args, **kwargs):
dealer_id = kwargs['pk']
dealer_employees = User.objects.filter(dealer_id=dealer_id)
if 'search_employee' in request.GET:
filtered_name = request.GET.get('search_employee')
filtered_dealer_employees = dealer_employees.filter(first_name__icontains=filtered_name)
context = {'dealer': filtered_dealer_employees, 'form_employee': DealerEmployeeCreationForm,
'dealer_id': dealer_id,
'DEALER_ROLES_TRANSLATED': User.DEALER_ROLES_TRANSLATED,
'order_type': self.order_type}
return render(request, 'comprital/add_distributor_field.html', context)

Django: Create object with ForeignKey from url

I am working on a Django project with two models linked by a ForeignKey. The parent model, Composition, is linked to the child model, NoteObject, by the id of Composition.
in models.py
class Composition(models.Model):
id = models.AutoField(primary_key=True)
...
class NoteObject(models.Model):
id = models.AutoField(primary_key=True)
composition = models.ForeignKey(Composition, on_delete=models.CASCADE)
...
Once a composition is created, the user needs to be able to create NoteObjects that belong to that composition. The notes are created with the following method:
in views.py
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def get_context_data(self, **kwargs):
kwargs['notes'] = NoteObject.objects.filter(
composition=self.kwargs['composition'])
return super(NoteCreateView, self).get_context_data(**kwargs)
The get_context_data method is there to display only the notes for the current composition. The current composition comes from the id of the composition that is part of the url where <composition> is the id of the composition.
in urls.py
path('entry/<composition>/', views.NoteCreateView.as_view(), name='entry')
When I save a NoteObject, what do I need to do in order to set the value of the ForeignKey to be the value within <composition>?
in models.py
def save(self, *args, **kwargs):
composition_id = ????????
self.composition_id = composition_id
super(NoteObject, self).save(*args, **kwargs)
How do I get the value of kwarg in the CreateView to be the ForeignKey when the object is created?
I think you can do it by over-riding form_valid method as mentioned in docs here.
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def form_valid(self, form):
form.instance.composition = self.kwargs['composition']
return super(NoteCreateView, self).form_valid(form)
In order to make this work, you need to override the dispatch function as well.
def dispatch(self, request, *args, **kwargs):
self.composition = Composition.objects.values_list(
'id').filter(pk=kwargs['composition'])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.composition_id = self.composition
return super().form_valid(form)
I used this solution and modified it slightly: CreateView Set ForeignKey from Url Parameter.

Django Admin - Filter ManyToManyField with through model

How can I filter a queryset inside the Admin page of an object that has a ManyToManyField relation with a manually defined through model?
Given models.py
class Foo(models.Model):
foo_field1 = models.CharField(max_length=50)
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo, through="FooBar")
class FooBar(models.Model):
main = models.ForeignKey(Main, on_delete=models.CASCADE)
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
new_field = models.CharField(max_length=50)
Inside admin.py
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
class MainAdmin(admin.ModelAdmin):
inlines = [M2MInlineAdmin,]
...
def formfield_for_manytomany(self, db_field, request, **kwargs):
print('called formfield_for_manytomany')
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_field_queryset(self, db, db_field, request):
print('called get_field_queryset')
return super().get_field_queryset(db, db_field, request)
I try to access both of these methods, but none of them are called if I specify a through table. However, they do get called if the ManyToMany relation is simply defined as like this:
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo)
Is there a method to filter the queryset when a through table is specified (while being able to access the request context)?
EDIT:
The methods are indeed called when the ManyToManyField has a through model specified, only if there are no fieldsets specified inside the modelAdmin class.
How to access these methods when fieldsets are defined?
formfield_for_manytomany method seems to be called only when default form is used. When fieldsets is defined, it is using a different form which is why above method is not getting called.
Since you are using tabular admin for many to many field, you can override get_queryset to filter with field.
class M2MInlineAdmin(admin.TabularInline):
model = Main.fruits.through
extra = 1
def get_queryset(self, request):
qs = super(M2MInlineAdmin, self).get_queryset(request)
qs = qs.filter(some_arg=some_value)
return qs
Alternatively, you can write a custom model form and use it in admin instead of default form.
class MainAdminForm(forms.ModelForm):
class Meta:
model = Main
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# custom setup
class MainAdmin(admin.ModelAdmin):
form = MainAdminForm
You can use the formfield_for_foreignkey() method on the inline class.
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

DeleteView marking inactive instead deleting?

Is there some elegant solution to using Django's DeleteView but instead actually deleting the objects, marking them inactive? I like the simplicity of the DeleteView but would want to keep the data at the backend, instead of removing it.
Elegant solution would be overriding Model & Manager to update a field on delete. This is an implementation as Abstract Model, so that it can be extended by any other Model. You can modify it as per your need, if you already have delete fields in your model.
Soft Deletion Abstract Model
class SoftDeletionModel(models.Model):
deleted_at = models.DateTimeField(blank=True, null=True)
objects = SoftDeletionManager()
all_objects = SoftDeletionManager(alive_only=False)
class Meta:
abstract = True
def delete(self):
self.deleted_at = timezone.now()
self.save()
def hard_delete(self):
super(SoftDeletionModel, self).delete()
Object Manager
class SoftDeletionManager(models.Manager):
def __init__(self, *args, **kwargs):
self.alive_only = kwargs.pop('alive_only', True)
super(SoftDeletionManager, self).__init__(*args, **kwargs)
def get_queryset(self):
if self.alive_only:
return SoftDeletionQuerySet(self.model).filter(deleted_at=None)
return SoftDeletionQuerySet(self.model)
def hard_delete(self):
return self.get_queryset().hard_delete()
QuerySet
class SoftDeletionQuerySet(QuerySet):
def delete(self):
return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now())
def hard_delete(self):
return super(SoftDeletionQuerySet, self).delete()
def alive(self):
return self.filter(deleted_at=None)
def dead(self):
return self.exclude(deleted_at=None)
For explanation, see Soft Deletion in Django
The DeleteView inherits DeletionMixin so you can just predefine the delete method.
DeletionMixin
Override delete method in DeleteView as follows
class Example(DeleteView):
def delete(self, request, *args, **kwargs):
"""
Calls the delete() method on the fetched object and then
redirects to the success URL.
"""
self.object = self.get_object()
self.object.is_deleted = True # Declare a boolean field is_deleted in your model. Default value is Flase.
return HttpResponseRedirect(self.get_success_url())

Django - Problems with get_or_create()

I'm facing problems using get_or_create() in my view.
What I want to do is have the User get or create an instance of the Keyword model whenever he wants to add a keyword.
I have a Keyword model that looks like this:
class Keyword(models.Model):
word = models.CharField(max_length=30, unique=True, default=None)
members = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, default=None)
def __str__(self):
return self.keywords
I have a form to create the keyword:
class KeywordForm(forms.ModelForm):
keywords = forms.CharField(max_length=30)
def __init__(self, *args, **kwargs):
super(KeywordForm, self).__init__(*args, **kwargs)
self.fields["keywords"].unique = False
class Meta:
fields = ("keywords",)
model = models.Keyword
I've tried different things in the view and here is my current version, without the use of get_or_create. It only creates the keyword:
class KeywordCreationView(LoginRequiredMixin, generic.CreateView):
form_class = forms.KeywordForm
model = models.Keyword
page_title = 'Add a new keyword'
success_url = reverse_lazy("home")
template_name = "accounts/add_keyword.html"
def form_valid(self, form):
var = super(KeywordCreationView, self).form_valid(form)
self.object.user = self.request.user
self.object.save()
self.object.members.add(self.object.user)
return var
How should my view look in order to get the keyword if it exists and if it does, add the User as 'member'. If it doesn't, create the Keyword.
Thanks for your help!
I do believe CreateView isn't the right class for this. You should use UpdateView instead and override the get_object method (which is actually a part of the SingleObjectMixin) ancestor of this class based view.
The source code of this mixin is rather daunging but in your case something as simple as
def get_object(self, queryset=None):
pk = self.kwargs.get(self.pk_url_kwarg)
if queryset:
obj, c = queryset.get_or_create(pk=pk)
else:
obj, c = MyModel.get_or_create(pk=pk)
return obj
might work. But frankly, it's alot simpler to use a simple (non class based view)

Categories

Resources