I'm trying to implement a DeleteView and prevent the model from being deleted if the user is not the owner. I'm sure it's a pretty trivial task but i can't manage to find a clear answer. So far I can see my view but currently everyone can delete the object.
Here is my code :
views.py :
#method_decorator(login_required, name='dispatch')
class RestaurantDeleteView(DeleteView):
model = Restaurant
template_name = 'restaurant/delete_form.html'
success_url = '/'
models.py :
class Restaurant(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
category = models.CharField(max_length=200)
description = models.TextField()
capacity = models.IntegerField(default=0)
def get_absolute_url(self):
return reverse('detail', kwargs={'pk': self.pk})
Thanks in advance !
This might not be the best way, but it illustrates the point.
In your views.py, you can access self.get_object() to get the model instance in reference, and then check to see if instance.owner == self.request.user. Look at the source code and try to remain faithful as much possible to the original. All you need is a conditional check.
Something like this:
#method_decorator(login_required, name='dispatch')
class FooDeleteView(DeleteView):
model = Foo
success_url = reverse_lazy('index')
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.owner != self.request.user:
return redirect(self.success_url)
return super().post(request, *args, **kwargs)
If you are curious and would like to know more, you can import pdb and place pdb.set_trace() on the first line of the method, and explore what self is and what methods is available to it. You can do this by callin dir(self) once you inside the method call. You'd be surprised with the amount of things you'd discover.
Related
Some info
I'm overriding the form_valid method (for one reason or another).
What I'm trying to do
I want to test the form_valid instance, and specifically its arguments. To do so, I'm using Django's test client.
Some code
models.py:
class MyModel(models.Model):
my_model_text = models.CharField(max_length=100)
my_model_date = models.DateTimeField(
'my model date',
auto_now_add=True)
views.py:
class CreateMyModelView(LoginRequiredMixin, generic.edit.CreateView):
model = MyModel
template_name = 'myapp/create-my-model.html'
form_class = CreateMyModelForm
def post(self, request=None, *args, **kwargs):
# do something here
form = self.get_form()
if form.is_valid():
return self.form_valid(form, request)
else:
return self.form_invalid(form)
def form_valid(self, form, request):
# do something else here
my_model_text = form.cleaned_data['my_model_text']
MyModel.objects.create(my_model_text=my_model_text)
return redirect(reverse('myapp:mypage'))
forms.py:
class CreateMyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['my_model_text']
tests.py:
class CreateMyModelViewTests(TestCase):
#classmethod
def setUpTestData(cls):
cls.my_auth_user = User.objects.create(
username='my_auth_user')
cls.my_auth_user.set_password('my_auth_password')
cls.my_auth_user.save()
def test_form_valid(self):
client = self.client
client.login(
username='my_auth_user',
password='my_auth_password')
# post response ? #
Question
How can I test the arguments that form_valid gets with the test-client (not an instance of the form object, but the form_valid method itself)?
Since CBVs store the current request as self.request, you don't need to change the signature of form_valid.
I think your view can/should be simplified to something like this (and as you can see, there's just commented-out stuff in form_valid() too, so you can elide it entirely unless you need to modify the instance using e.g. the request user).
class CreateMyModelView(LoginRequiredMixin, CreateView):
model = MyModel
template_name = 'myapp/create-my-model.html'
form_class = CreateMyModelForm
success_url = reverse_lazy('myapp:mypage')
def form_valid(self, form):
# You can modify the object-to-be-saved before saving, e.g.
# form.instance.creator = self.request.user
# The CreateView implementation just calls `form.save()` and redirects away,
# so let's reuse that.
return super().form_valid(form)
Update
Thanks to Michael I was able to get this to work perfectly in my CreateView, but not in the UpdateView. When I try to set a form_class it spits out an improperly configured error.
How can I go about filtering the ForeignKey in the updateview?
End Update
I have a feeling I'm missing something small here but I've been working on it for a while and can't figure it out.
I have an app called story universe where the user creates one with a name and description.
I then have a character creator class where the user can create a character within that universe. This all works fine, except when the user goes to create their character they see a list of all universes created by all users.
Then there are other apps that will also mimic what I'm trying to do with the character creator.
I need to limit the Story Universes to only those created by the currently logged in user.
I've tried a few different ways and had the most success with the following, but with this code, no Universe appears when trying to create a new character.
models.py:
class Universe(models.Model):
user = models.ForeignKey(User,related_name='universe',on_delete=models.CASCADE)
name = models.CharField(max_length=100, unique=True)
description = models.TextField(max_length=2000,blank=True,default="")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('universe:singleuniverse',kwargs={'pk': self.pk})
class Meta:
ordering = ['name']
unique_together = ['user','name']
class Character(models.Model):
user = models.ForeignKey(User,related_name='characters',on_delete=models.CASCADE)
universe = models.ForeignKey("story_universe.Universe", on_delete=models.CASCADE)
name = models.CharField(max_length=255,unique=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('character_developer:singlecharacter',kwargs={'pk': self.pk})
class Meta():
ordering = ['name']
unique_together=['user','name']
views.py:
class CreateCharacter(LoginRequiredMixin,generic.CreateView):
template_name ='character_developer/character_create.html'
form_class = CreateForm
def get_form_kwargs(self):
kwargs = super(CreateCharacter,self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self,form):
self.object = form.save(commit=False)
self.object.user = self.request.user
self.object.save
return super().form_valid(form)
forms.py:
class CreateForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
user = kwargs.pop('user')
super(CreateForm,self).__init__(*args,**kwargs)
self.fields['universe'].queryset = Character.objects.filter(user=user)
class Meta:
model = Character
fields = ('universe','name')
You need to make a slight change to the CreateForm class in your forms.py:
class CreateForm(forms.ModelForm):
def __init__(self,*args,**kwargs):
user = kwargs.pop('user')
super(CreateForm,self).__init__(*args,**kwargs)
self.fields['universe'].queryset = Universe.objects.filter(user=user)
class Meta:
model = Character
fields = ('universe','name')
That will then pull through the returned Universe objects into the universe field, but only for the currently logged in user.
I'm in the process of debugging my custom permissions class and returning a value of False for my has_object_permission() function, but my I'm still able to access my API (GET request), via Restframework's API browser without authenticating and I can't understand why. Any help would be greatly appreciated. Please see code below. for whatever reasons, it appears that my has_object_permission function is not executing. Please Help
urls.py
router = BulkRouter()
router.register(r'api1', SimpleViewSet1)
urlpatterns = [
url(r'^test/', include(router.urls, namespace='api1')),
]
views.py
class SimpleViewSet1(generics.BulkModelViewSet):
queryset = Barcode.objects.all()
permission_classes = (MyUserPermission,)
serializer_class = SimpleSerializer1
def get_queryset(self):
user = User.objects.get(pk=2)
return Barcode.objects.filter(owner = user)
def get_object(self):
obj = get_object_or_404(self.get_queryset())
self.check_object_permissions(self.request, obj)
return obj
permissions.py
class MyUserPermission(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return False
serializer.py
class SimpleSerializer1(BulkSerializerMixin, # only required in DRF3
ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta(object):
model = Barcode
# only required in DRF3
list_serializer_class = BulkListSerializer
fields = ('barcode_number', 'barcode_type', 'owner')
models.py
#python_2_unicode_compatible
class Barcode(models.Model):
owner = models.ForeignKey('auth.User', related_name = 'barcodes')
barcode_number = models.CharField(max_length=200)
barcode_type = models.CharField(max_length=200)
def __str__(self):
return self.barcode_number
Django Rest API Guide says:
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.
rest_framework.generics.BulkModelViewSet, as it's name suggests,does bulk operations. It means that you have to use object-level filtering as proposed in the docs.
You should be looking especially under this section. Pay close attention to the example and make use of the code. You should also read about the DjangoModelPermissions to understand how does the example in the link above works.
I have a model with good validation, I'm using the clean method inside the model. the problem is when I am validating I am using an object that has not been set in the form which raise an exception that the object is not there yet.
I want a solution to pass the object from url primary key to the form before any validation, so my clean method works fine.
Here is a similar example.
The main model
class Student(models.Model):
first_name = models.CharField(max_length=30)
lets sat that each student might have one semester at a time. However, if there are any semesters before then the start date must be after the last semester end date.
class Semester(models.Model):
student = models.OneToOneField(Student)
start_date = models.DateField()
def clean(self):
# do not allow the start date to be before last semester end date
if self.student.semesterhistory_set.all().count() > 0:
last_semester_end_date = self.student.semesterhistory_set.last().end_date
if last_semester_end_date >= self.start_date:
message = _("Start Date for this semester must be after %s" % last_date)
raise ValidationError(message)
class SemesterHistory(models.Model):
student = models.ForeignKey(Student)
start_date = models.DateField()
end_date = models.DateField()
In the view, I am passing the student object which will be used in validation after validating the form. (problem)
# URL for this is like this student/(pk)/semesters/create/
class SemesterCreate(CreateView):
model = Semester
fields = ['start_date']
def form_valid(self, form):
form.instance.student = get_object_or_404(Student, id=int(self.kwargs['pk']))
return super(SemesterCreate, self).form_valid(form)
Error:
RelatedObjectDoesNotExist Semester has no student
Obviously you need call form.save(commit=False) which returns instance ... Also semantically wrong approach raise 404 in form_valid...
class SemesterCreate(CreateView):
model = Semester
fields = ['start_date']
student = object = None
def dispatch(self, request, *args, **kwargs):
self.student = get_object_or_404(Student, id=kwargs['pk'])
return super(SemesterCreate, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.student = self.student
self.object.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('...')
https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#the-save-method
Actually to have a clean set I would add a custom ModelForm. I also use CreateView and this is how i use it .
First add a custom ModelForm (I personnaly add a forms.py file in my apps):
from django.contrib.auth.forms import UserCreationForm
from .model import Semester
class CreateSemesterForm(UserCreationForm):
error_messages = {
'last_date': _("some message"),
}
class Meta:
model = Semester
fields = ('some', 'fields') #or __all__
def clean_your_date_field(self):
#clean_name_field will be called when the form_valid will be called with all clean_fields functions
#here you define your clean method and raise Validation error
return field
def save(self, commit=True):
semester = super(CreateSemesterForm, self).save(commit=False)
#here you can set some other values
if commit:
semester.save()
return semester
And in your custom CreateView you have to add :
class SemesterCreate(CreateView):
form_class = CreateArtistForm
As you set model and fields in the ModelForm you can remove fields and model args from CreateView.
You also can override form_valid in your Custom ModelForm.
Now CreateView will call form_valid which call all clean functions, and if it's all passes, it returns and save your semester.
I came across this yesterday after facing the exact same issue with my project.
It's been a couple of years since you've posted this, but figure I'd post my solution to help anyone else out who might stumble across this.
The solution I came across is to use a custom modelform:
from django import forms
from .models import Blade
class SemesterForm(forms.ModelForm):
class Meta:
model = Semester
fields = '__all__'
widgets = {'student': forms.HiddenInput()}
And the in your view:
class SemesterCreate(CreateView):
model = Semester
def get_initial(self, **kwargs):
# get current student from pk in url
current_student = get_object_or_404(Student,
pk=self.kwargs.get('pk'))
return { 'site': current_student }
The trick here is that you must set the student field to hidden in the form. That way, it will keep the initialised value you give it in the view, but won't be available to the user.
So, it will be there when the form is submitted and the full_clean() method is called (which will then call the clean() method on the model), and your nice and tidy validations performed in the model clean() will work.
I have two apps in Django where one app's model (ScopeItem) on its instance creation must create an instance of the other app's model as well (Workflow); i.e. the ScopeItem contains it's workflow.
This works nicely when tried from the shell. Creating a new ScopeItem creates a Workflow and stores it in the ScopeItem. In admin I get an error, that the workflow attribute is required. The attribute is not filled in and the model definition requires it to be set. The overwritten save method though does this. Hence my question is, how to call save before the check in admin happens?
If I pick an existing Workflow instance in admin and save (successfully then), then I can see that my save method is called later and a new Workflow is created and attached to the ScopeItem instance. It is just called too late.
I am aware that I could allow empty workflow attributes in a ScopeItem or merge the ScopeItem and the Workflow class to avoid the issue with admin. Both would cause trouble later though and I like to avoid such hacks.
Also I do not want to duplicate code in save_item. Just calling save from there apparently does not cut it.
Here is the code from scopeitems/models.py:
class ScopeItem(models.Model):
title = models.CharField(max_length=64)
description = models.CharField(max_length=4000, null=True)
workflow = models.ForeignKey(Workflow)
def save(self, *args, **kwargs):
if not self.id:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
super(ScopeItem, self).save(*args, **kwargs)
And workflow/models.py:
from django.utils.timezone import now
class Workflow(models.Model):
PENDING = 0
APPROVED = 1
CANCELLED = 2
STATUS_CHOICES = (
(PENDING, 'Pending'),
(APPROVED, 'Done'),
(CANCELLED, 'Cancelled'),
)
description = models.CharField(max_length=4000)
status = models.IntegerField(choices=STATUS_CHOICES)
approval_date = models.DateTimeField('date approved', null=True)
creation_date = models.DateTimeField('date created')
update_date = models.DateTimeField('date updated')
def save(self, *args, **kwargs):
if not self.id:
self.creation_date = now()
self.update_date = now()
super(Workflow, self).save(*args, **kwargs)
In scopeitems/admin.py I have:
from django.contrib import admin
from .models import ScopeItem
from workflow.models import Workflow
class ScopeItemAdmin(admin.ModelAdmin):
list_display = ('title', 'description', 'status')
list_filter = ('workflow__status', )
search_fields = ['title', 'description']
def save_model(self, request, obj, form, change):
obj.save()
def status(self, obj):
return Workflow.STATUS_CHOICES[obj.workflow.status][1]
admin.site.register(ScopeItem, ScopeItemAdmin)
You could set the field blank=True on workflow.
You said you don't want to allow "empty workflow attributes in a ScopeItem." Setting blank=True is purely validation-related. Thus, on the backend workflow will still be NOT NULL. From the Django docs:
If a field has blank=True, form validation will allow entry of an empty value.
Referring to your example you should be able to use:
workflow = models.ForeignKey(Workflow, blank=True)
You need to exclude the field from the form used in the admin, so that it won't be validated.
class ScopeItemForm(forms.ModelForm):
class Meta:
exclude = ('workflow',)
model = ScopeItem
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
#Daniel Roseman's answer is correct as long as you don't need to edit the workflow field in admin at any time. If you do need to edit it then you'll need to write a custom clean() method on the admin form.
forms.py
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
if 'pk' not in self.instance:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
return cleaned_data
admin.py
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemAdminForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
Answering my own question:
As #pcoronel suggested, the workflow attribute in ScopeItem must have blank=True set to get out of the form in the first place.
Overwriting the form's clean method as suggested by #hellsgate was also needed to create and store the new Workflow.
To prevent code duplication I added a function to workflow/models.py:
def create_workflow(title="N/A"):
workflow = Workflow(
description='ScopeItem %s workflow' % title,
status=Workflow.PENDING)
workflow.save()
return workflow
This makes the ScopeItemAdminForm look like this:
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
cleaned_data['workflow'] = create_workflow(cleaned_data['title'])
return cleaned_data
Additionally I changed the save method in scopeitems/models.py to:
def save(self, *args, **kwargs):
if not self.id:
if not self.workflow:
self.workflow = create_workflow(self.title)
super(ScopeItem, self).save(*args, **kwargs)