Django - disable model editing - django

Is there a way, hopefully without breaking admin, to disable editing existing model instances on the ORM level?
I'm not talking about removing 'Save' and 'Save and continue' buttons from templates - there should be no operations that can change the values of a committed instance of a model.
Preferably, the 'Save As' option should work instead.

Overwrite the save function for your model like so:
class MyModel(models.Model):
def save(self, *args, **kwargs):
if self.pk is None:
super(MyModel, self).save(*args, **kwargs)
This function only call the superclass save function (which actually saves the change) if there is no pk, e.g. the model instance is new.

You could override your model class's save() (do nothing if self.pk) and delete (always do nothing)
But really, the database level is the safest place for that. For example, in PostgreSQL you could write two simple rules:
CREATE RULE noupd_myapp_mymodel AS ON UPDATE TO myapp_mymodel
DO NOTHING;
CREATE RULE nodel_myapp_mymodel AS ON DELETE TO myapp_mymodel
DO NOTHING;
Either way, the admin wouldn't know anything about this, so everything still looks editable. See my answer to Whole model as read-only for an attempt at making a model read-only in the admin. For your purposes, keep the add permission as-is, and only declare all fields read-only when not adding.
EDIT: One reason why overriding delete() in your model class is not safe, is the fact that "bulk delete" (Queryset.delete(), e.g. admin checkboxes action) will not call the individual instances' delete() method, it will go straight to SQL: https://docs.djangoproject.com/en/dev/topics/db/queries/#deleting-objects

For those who need to prevent MyModel.objects.filter(pk=123).update(name="bob"):
class NoUpdateQuerySet(models.QuerySet):
def update(self, *args, **kwargs):
pass
class MyModel(models.Model):
objects = NoUpdateQuerySet.as_manager()
...
Django docs - link

Related

Deleting the m2m relationship instead of the the object itself in Django via REST API

I have 3 models: User, UserItem (the m2m thourgh), and Item.
A User can create an Item. This automatically creates a UserItem.
A different User can see that Item, and add it to their own list of items, creating another UserItem.
If that first User wants to delete the Item, the other User won't be happy - it needs to stay, but appear gone for the initial User. However, if there's only one User still related to it, then the Item is safe to delete, and should be deleted to avoid filling the database with dead records.
This is how I think it should be handled:
Item delete call made to API from User
Item pre_delete checks if item.user_set > 1
If True, manually delete the UserItem, leave Item where it is. If False, delete the Item
This way UserItem isn't exposed via the API, and management for a client is simplified.
Is this the right/common way to go? How can it be done with Django? I'm unsure how to prevent Item.delete() from happening within pre_delete without raising an exception, but as this is expected behaviour raising an exception doesn't seem like the right way to do this.
This seems fine to me. But instead of working with signals, you could override the delete() method on the Item model. See the official documentation for an example with the save() method. Your delete() method could be implemented likewise, i.e. that it wont call the super() when there are still other Users with a UserItem.
From django docs: "If you want customized deletion behavior, you can override the delete() method."
I think it's what you want:
def delete(self, *args, **kwargs):
if item.user_set > 1:
return
else:
super(Item, self).delete(*args, **kwargs) # Call the "real" delete() method.
Here's what I went with. It keeps the logic in the model, but the view gives it the current user.
I thought it best to keep out of delete() because an admin user should be able to delete an Item regardless of related users, and there is no simple way to access the current user within delete().
Constructive criticism welcome!
models.py
class Item(TimeStampedModel):
...
def delete_item_or_user_item(self, user):
"""
Delete the Item if the current User is the only User related to it.
If multiple Users are related to the Item, delete the UserItem.
"""
if UserItem.objects.filter(item=self).count() > 1:
UserItem.objects.filter(item=self, user=user).delete()
else:
self.delete()
views.py
class ItemViewSet(viewsets.ModelViewSet):
...
def perform_destroy(self, instance):
instance.delete_item_or_user_item(self.request.user)

Restrict a model to access only rows with a specific condition?

I want to use a Django model to access a subset of database rows. Working with a number of legacy databases, I'd rather not create views to the database, if possible.
In short, I'd like to tell my model that there's field foo which should always have the value bar. This should span any CRUD operation for the table, so that newly created rows would also have foo=bar. Is there a simple Django way for what I'm trying to achieve?
UPDATE: I want to ensure that this model doesn't write anything to the table where foo != bar. It must be able to read, modify or delete only those rows where foo=bar.
For newly created items you can set the default value in model definition
class MyModel(models.Model):
# a lot of fields
foo = models.CharField(max_length=10, default='bar')
# Set the manager
objects = BarManager()
def save(self, force_insert=False, force_update=False, using=None):
self.foo = 'bar'
super(MyModel, self).save(force_insert, force_update, using)
To achieve that MyModel.objects.all() should return only rows with foo=bar you should implement your custom manager. You can re-define the get_query_set method to add filtering.
class BarManager(models.Manager):
use_for_related_fields = True
def get_query_set(self):
return super(BarManager, self).get_query_set().filter(foo='bar')
Update after #tuomassalo comment
1) The custom manager will affect all calls to MyModel.objects.get(id=42) as this call just proxy a call to .get_query_set().get(id=42). To achieve this you have to set Manager as default manager for model (assign it to objects variable).
To use this manager for related lookups (e.g. another_model_instance.my_model_set.get(id=42)) you need to set use_for_related_fields = True on you BarManager. See Controlling automatic Manager types in the docs.
2) If you want to enforce foo=bar then default value is not enough for you. You can either use pre_save signal or overwrite the save method on your model. Don't forget to call the original save method.
I updated the MyModel example above.

Why is adding site to an object doesn't seem to work in a save() override in the Django admin?

I have overrided the save() method of one of my model so it can inherit from the sites object and tags from its parent.
def save(self, *args, **kwargs):
ret = models.Model.save(self, *args, **kwargs)
if self.id:
for site in self.parent.sites.all():
self.sites.add(site.id)
for tag in self.parent.tags_set.all():
Tag.objects.add_tag(self, tag)
Using ipdb, I can see that self.sites.all() DOES return 4 sites at the end of the method, but strangely, once the request is finish, the same self.sites.all() does not return anything anymore.
I don't use transactions (at least explicitly), and I'm using Django 1.3 and Ubuntu 11.04
EDIT: found out that it works anywhere but in the admin. Doesn't the admin call save? If not, how can I hook to the object creation / update?
EDIT2: tested, and does call save. I have print statements to prove it. But it doesn't add the sites. It's a mystery.
In fact, this is a problem about adding programatically many to many relationships when saving a model if you use the Django admin.
Django save m2m relationships in the admin by calling 'clear' to wipe them out, then setting them again. It means that the form destroy any attached data (including your programatically attached) to the object then add the ones you entered in the admin.
It works outside the admin because we don't use the admin form that clear the m2m relationship.
The reason it works for tags in the admin is that the tagging application doesn't use m2m but emulate it by placing a TaggedItem object with a foreign key to a tag and to your model with a generic relation. Plus it's an inline field inclusion.
I tried a lot of things and finally had to look at the Django source code to realize that Django does not process admin forms in the usual way. What it does it:
call ModelAdmin.save_form: it calls form.save with commit = False, returning an unsaved instance and adding a save_m2m method to the form.
call ModelAdmin.save_model that actually calls the instance save method.
call form.save_m2m
Therefor:
you can't override your save method since save_m2m is called after and clear the m2m relations.
you can't override save_model for the same reason.
you can't override save_m2m because it is added by monkey patch to the form model in form.save, erasing you own method definition.
I didn't find a clean solution, but something that works is:
Provide a form for the ModelAdmin class with a method to override save_m2m with your own method:
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
def set_m2m_method(self, update_tags_and_sites=True):
alias = self.save_m2m
def new_save_m2m(): # this is your new method
alias() # we need to call the original method as well
self.instance.add_niche_sites_and_tags()
self.save_m2m = new_save_m2m # we erase Django erasing :-)
Call this method in a ModelAdmin.model_save override:
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
def save_model(self, request, obj, form, change):
obj.save()
form.set_m2m_method()
This cause the following:
Django calls save_model, replacing its monkey patch by yours
django calls our form.save_m2m that first call its old method that clears relations, then attach the m2m to the object.
I'm completely open to any better way to do this as this is twisted and plain ugly.
Since the problem seems to be reserved to admin, I tried to add some logic to do this in the ModelAdmin's save_model method, but it doesn't seem to help at all:
class SomeModelAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
for site in Site.objects.all():
obj.sites.add(site.id)
print obj.sites.all()
Oddly print obj.sites.all() does list all the sites, however, they don't stay saved. Some sort of M2M issue perhaps?

Django: Read only field

How do I allow fields to be populated by the user at the time of object creation ("add" page) and then made read-only when accessed at "change" page?
The simplest solution I found was to override the get_readonly_fields function of ModelAdmin:
class TestAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
'''
Override to make certain fields readonly if this is a change request
'''
if obj is not None:
return self.readonly_fields + ('title',)
return self.readonly_fields
admin.site.register(TestModel, TestAdmin)
Object will be none for the add page, and an instance of your model for the change page.
Edit: Please note this was tested on Django==1.2
There's two thing to address in your question.
1. Read-only form fields
Doesn't exist as is in Django, but you can implement it yourself, and this blog post can help.
2. Different form for add/change
I guess you're looking for a solution in the admin site context (otherwise, just use 2 different forms in your views).
You could eventually override add_view or change_view in your ModelAdmin and use a different form in one of the view, but I'm afraid you will end up with an awful load of duplicated code.
Another solution I can think of, is a form that will modify its fields upon instantiation, when passed an instance parameter (ie: an edit case). Assuming you have a ReadOnlyField class, that would give you something like:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = Stuff
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
if kwargs.get('instance') is not None:
self.fields['title'] = ReadOnlyField()
In here, the field title in the model Stuff will be read-only on the change page of the admin site, but editable on the creation form.
Hope that helps.
You can edit that model's save method to handle such a requirement. For example, you can check if the field already contains some value, if it does, ignore the new value.
One option is to override or replace the change_form template for that specific model.

How to prevent self (recursive) selection for FK / MTM fields in the Django Admin

Given a model with ForeignKeyField (FKF) or ManyToManyField (MTMF) fields with a foreignkey to 'self' how can I prevent self (recursive) selection within the Django Admin (admin).
In short, it should be possible to prevent self (recursive) selection of a model instance in the admin. This applies when editing existing instances of a model, not creating new instances.
For example, take the following model for an article in a news app;
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField()
related_articles = models.ManyToManyField('self')
If there are 3 Article instances (title: a1-3), when editing an existing Article instance via the admin the related_articles field is represented by default by a html (multiple)select box which provides a list of ALL articles (Article.objects.all()). The user should only see and be able to select Article instances other than itself, e.g. When editing Article a1, related_articles available to select = a2, a3.
I can currently see 3 potential to ways to do this, in order of decreasing preference;
Provide a way to set the queryset providing available choices in the admin form field for the related_articles (via an exclude query filter, e.g. Article.objects.filter(~Q(id__iexact=self.id)) to exclude the current instance being edited from the list of related_articles a user can see and select from. Creation/setting of the queryset to use could occur within the constructor (__init__) of a custom Article ModelForm, or, via some kind of dynamic limit_choices_to Model option. This would require a way to grab the instance being edited to use for filtering.
Override the save_model function of the Article Model or ModelAdmin class to check for and remove itself from the related_articles before saving the instance. This still means that admin users can see and select all articles including the instance being edited (for existing articles).
Filter out self references when required for use outside the admin, e.g. templates.
The ideal solution (1) is currently possible to do via custom model forms outside of the admin as it's possible to pass in a filtered queryset variable for the instance being edited to the model form constructor. Question is, can you get at the Article instance, i.e. 'self' being edited the admin before the form is created to do the same thing.
It could be I am going about this the wrong way, but if your allowed to define a FKF / MTMF to the same model then there should be a way to have the admin - do the right thing - and prevent a user from selecting itself by excluding it in the list of available choices.
Note: Solution 2 and 3 are possible to do now and are provided to try and avoid getting these as answers, ideally i'd like to get an answer to solution 1.
Carl is correct, here's a cut and paste code sample that would go in admin.py
I find navigating the Django relationships can be tricky if you don't have a solid grasp, and a living example can be worth 1000 time more than a "go read this" (not that you don't need to understand what is happening).
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['myManyToManyField'].queryset = MyModel.objects.exclude(
id__exact=self.instance.id)
You can use a custom ModelForm in the admin (by setting the "form" attribute of your ModelAdmin subclass). So you do it the same way in the admin as you would anywhere else.
You can also override the get_form method of the ModelAdmin like so:
def get_form(self, request, obj=None, **kwargs):
"""
Modify the fields in the form that are self-referential by
removing self instance from queryset
"""
form = super().get_form(request, obj=None, **kwargs)
# obj won't exist yet for create page
if obj:
# Finds fieldnames of related fields whose model is self
rmself_fields = [f.name for f in self.model._meta.get_fields() if (
f.concrete and f.is_relation and f.related_model is self.model)]
for fieldname in rmself_fields:
form.base_fields[fieldname]._queryset =
form.base_fields[fieldname]._queryset.exclude(id=obj.id)
return form
Note that this is a on-size-fits-all solution that automatically finds self-referencing model fields and removes self from all of them :-)
I like the solution of checking at save() time:
def save(self, *args, **kwargs):
# call full_clean() that in turn will call clean()
self.full_clean()
return super().save(*args, **kwargs)
def clean(self):
obj = self
parents = set()
while obj is not None:
if obj in parents:
raise ValidationError('Loop error', code='infinite_loop')
parents.add(obj)
obj = obj.parent