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())
Related
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.
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)
I am using Django REST Framework to create an API for my web app. I have a class 'Comment', that has depth=2 set in the Meta class. This works great when GETing the Comments. When I try to send a POST or PUT request though (i.e. create a new Comment) I am told I need to include objects instead of ForeignKey IDs.
Here's my Serializer class:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
The model:
class Comment(models.Model):
user = models.ForeignKey(User, null=True, blank=True,
related_name='comments')
budget = models.ForeignKey(Budget, related_name='comments')
published = models.BooleanField(default=False)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
The view code:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentSerializer
def pre_save(self, obj):
obj.user = self.request.user
And the error that is displayed in the output (JSON) is:
{"user": ["This field is required."], "budget": [{"non_field_errors": ["Invalid data"]}]}
When this raw data is sent:
{"budget": 2, "published": true, "body": "Another comment"}
I know this is a little bit late but I ended up using 2 serializers like so:
class CommentReadSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
class CommentWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
Then used like this:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentReadSerializer
def create(self, request, *args, **kwargs):
serializer = CommentWriteSerializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
serializer = CommentReadSerializer(serializer.object)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
You can set different serializers by overriding the get_serializer_class() function, like so:
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return YourWriteSerializer
else:
return YourReadSerializer
I thought to add this one, since i came here from Googling after a while.
I believe the proper way to define a serializer field that refers to a foreign key relationship is through something like serializers.PrimaryKeyRelatedField. I don't believe that model serializers automatically use this field class without defining it explicitly in the serializer class.
http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
I would imagine that a PrimaryKeyRelatedField serializer would correctly handle JSON data submissions like the one you used in your example.
I had the same problem so I Solved making custom generic methods.This is better implementation of above answers
class CustomListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
Similarily RUD,
class CustomRetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) # enter code here
Now I just give writeSerializers and readSerializers values in Views.py
Also to create Read-write Serializers there is an easy way.
class employeeWriteSerializer(serializers.ModelSerializer):
class Meta:
model = employee
fields = ('username','email',..)
class employeeReadSerializer(serializers.ModelSerializer):
class Meta(employeeWriteSerializer.Meta):
depth = 1
It saves time and repetitive work you can also add authentication classes in custom generic Api(Retitve work). Thanks.
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.
I have a django app with the following class in my admin.py:
class SoftwareVersionAdmin(ModelAdmin):
fields = ("product", "version_number", "description",
"media", "relative_url", "current_version")
list_display = ["product", "version_number", "size",
"current_version", "number_of_clients", "percent_of_clients"]
list_display_links = ("version_number",)
list_filter = ['product',]
I want to have these fileds for add page but different fields for change page. How can I do that?
This is an old question but I wanted to add that the add_view and change_view methods can be modified for this purpose:
class SoftwareVersionAdmin(ModelAdmin):
...
def add_view(self,request,extra_content=None):
self.exclude = ('product','version_number',)
return super(SoftwareVersionAdmin,self).add_view(request)
def change_view(self,request,object_id,extra_content=None):
self.exclude = ('product','description',)
return super(SoftwareVersionAdmin,self).change_view(request,object_id)
First have a look at source of ModelAdmin class' get_form and get_formsets methods located in django.contrib.admin.options.py. You can override those methods and use kwargs to get the behavior you want. For example:
class SoftwareVersionAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
# Proper kwargs are form, fields, exclude, formfield_callback
if obj: # obj is not None, so this is a change page
kwargs['exclude'] = ['foo', 'bar',]
else: # obj is None, so this is an add page
kwargs['fields'] = ['foo',]
return super(SoftwareVersionAdmin, self).get_form(request, obj, **kwargs)
I couldn't get this working in django 1.6.5 using the above solutions. So I tried creating forms and having get_form serve those pre-defined forms depending on if the object exists or not:
models.py:
from django.db import models
class Project(models.Model):
name = models.CharField('Project Name', max_length=255)
slug = models.SlugField('Project Slug', max_length=255, unique=True)
forms.py:
from django import forms
from models import Project
class ProjectAddForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = Project
class ProjectEditForm(forms.ModelForm):
class Meta:
model = Project
fields = ("name", 'slug')
admin.py
from django.contrib import admin
from models import Project
from forms import ProjectAddForm, ProjectEditForm
class ProjectAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
# Proper kwargs are form, fields, exclude, formfield_callback
if obj:
self.form = ProjectEditForm
else:
self.form = ProjectAddForm
return super(ProjectAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(Project, ProjectAdmin)
Now I can intercept the non-persistent test field in the forms clean and do as I wish with it, just overwrite clean in the ProjectAddForm:
def clean(self):
cleaned_data = super(ProjectAddForm, self).clean()
test = cleaned_data.get("test")
# Do logic here
#raise forms.ValidationError("Passwords don't match.")
return cleaned_data
I don't think it's a good idea to override fields or exclude or form, because they are config attributes, so they would not initialize for every request.
I think the accepted answer by shanyu is a good solution.
Or we can use the method from UserAdmin:
def get_fieldsets(self, request, obj=None):
if not obj:
return self.add_fieldsets
return super(UserAdmin, self).get_fieldsets(request, obj)
Remember to assign the add_fieldsets yourself. Unfortunately it doesn't fit my use case.
For Django 1.7. I don't know how they are implemented in other versions.
This is how it's done in Django 1.10. Just override get_form and return add_form when object is None:
class FoobarAddForm(forms.ModelForm):
class Meta:
model = Foobar
fields = ['some_field',]
#register(Foobar)
class AdminFoobar(admin.ModelAdmin):
add_form = FoobarAddForm
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super(AdminFoobar, self).get_form(request, obj, **defaults)
This specific code did not work for me.
I simply change it a little:
if obj: # obj is not None, so this is a change page
#kwargs['exclude'] = ['owner']
self.fields = ['id', 'family_name', 'status', 'owner']
else: # obj is None, so this is an add page
#kwargs['fields'] = ['id', 'family_name', 'status']
self.fields = ['id', 'family_name', 'status']
return super(YourAdmin, self).get_form(request, obj, **kwargs)
dpawlows' solution above is the clearest, I think.
However, I encountered an additional issue in that type of structure.
If change_view() makes changes to the model, e.g. specifies readonly_fields that have been filled in in add_view(), these changes persist in add_view() after change_view() has been called. For example:
def add_view(self, request, extra_context=None):
return super().add_view(request)
def change_view(self, request, object_id, extra_context=None):
self.readonly_fields = ['name'] # this change persists in add_view()
return super().change_view(self, request, object_id)
In this case, after change_view() has been called on any instance, invoking add_view() will show readonly_fields ("name", in this case) set by change_view() and thus protect these fields from filling in.
This can be solved by adding a 'roll back' assignment in add_view():
def add_view(self, request, extra_context=None):
self.readonly_fields = [] # 'roll back' for changes made by change_view()
return super().add_view(request)
Using formsets in Django 1.6 I ended up with the following:
def get_formsets(self, request, obj=None):
if obj is None:
# It's a new object
for field, fieldset in {'hide_me_from_the_first_fieldset': 0,
'me_from_the_second': 1,
'and_me_too': 1}.items():
self.fieldsets[fieldset][1]['fields'].remove(field)
return super().get_formsets(request, obj)
EDIT:
Perhaps a more intuitive way is to specify a separate add_fieldsets property and do:
def get_formsets(self, request, obj=None):
if obj is None:
self.fieldsets = self.add_fieldsets
return super().get_formsets(request, obj)
An easy way is to use fieldsets for the Change Page and add_fieldsets for the Add Page.
With more modern Django versions (3.2 at the time of writing), you can override some methods from BaseModelAdmin to achieve having different fields on the "change" and the "add" model admin page:
class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
"""Functionality common to both ModelAdmin and InlineAdmin."""
# ...
def get_exclude(self, request, obj=None):
"""
Hook for specifying exclude.
"""
return self.exclude
def get_fields(self, request, obj=None):
"""
Hook for specifying fields.
"""
if self.fields:
return self.fields
# _get_form_for_get_fields() is implemented in subclasses.
form = self._get_form_for_get_fields(request, obj)
return [*form.base_fields, *self.get_readonly_fields(request, obj)]
def get_fieldsets(self, request, obj=None):
"""
Hook for specifying fieldsets.
"""
if self.fieldsets:
return self.fieldsets
return [(None, {'fields': self.get_fields(request, obj)})]
def get_readonly_fields(self, request, obj=None):
"""
Hook for specifying custom readonly fields.
"""
return self.readonly_fields
For example, to add some read-only fields on the change page (obj exists), but not on the add page:
class MyModelAdmin(admin.ModelAdmin):
# Readonly_fields only on change page
def get_readonly_fields(self, request, obj=None):
if obj: # obj is not None, so this is a change page
return 'field_1', 'field_2'
return () # obj is None, so this is the add page