Django - Check Other Objects Prior to Save - python

I want to override the built-in django .save() method to perform a check against all other objects in the database.
For example:
class User(models.Model):
name = models.CharField(max_length=120)
class Admin(models.Model):
name = models.CharField(max_length=120)
class SecurityGroup(models.Model):
name = models.CharField(max_length=120)
users = models.ManytoManyField(User)
admins = models.ManytoManyField(Admin)
def save(self, *args, **kwargs):
# check admins don't exist in any other SecurityGroup prior to save
super(SecurityGroup, self).save(*args, **kwargs) # Call the "real" save() method.
The documentation example is pretty simple, and doesn't describe this type of pre-save check.
I have tried adding in lines to .save() such as:
`self.objects.filter(admins__name=self.admins.name).count()`
to call the other SecurityGroup objects but I receive the error:
`Manager is not accessible via SecurityGroup instance`
Is it possible to achieve this save functionality internal to the SecurityGroup Model, or do I need to create a form and use SecurityGroup.save(commit=False) for this type of pre-save check?
Thanks for the help.

The solution that worked for me was to override the Model's form in admin.py. This enabled a simple check whether admins already existed in a SecurityGroup or not.
from django.contrib import admin
from django.forms import ModelForm
from security.models import SecurityGroup
class SecurityGroupAdminForm(ModelForm):
class Meta:
model = SecurityGroup
fields = '__all__'
def clean(self):
# CHECK 1
if admins:
admins = self.cleaned_data['admins']
for a in admins:
existing_group = SecurityGroup.objects.filter(users__username=a.username)
if existing_group:
raise Exception("message")
return self.cleaned_data
Then, within the same admin.py file, indicate the custom form as part of the admin registration for the model of interest (in this case, SecurityGroup):
class UserSecurityGroupAdmin(admin.ModelAdmin):
# class Meta:
model = UserSecurityGroup
form = UserSecurityGroupAdminForm
admin.site.register(UserSecurityGroup, UserSecurityGroupAdmin)

The error is caused by accessing the Manager of a model through a model instance. You should have used
self.model_class().objects

Related

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')

Indirect inline in Django admin

I have the following models:
class UserProfile(models.Model):
user = models.OneToOneField(User)
class Property(models.Model):
user = models.ForeignKey(User)
I would like to create a TabularInline displaying every Property connected to a particular UserProfile on its Django admin page. The problem here is, of course, that Property does not have a ForeignKey directly to UserProfile, so I cannot simply write
class PropertyTabularInline(admin.TabularInline):
model = Property
class UserProfileAdmin(admin.ModelAdmin):
inlines = (PropertyTabularInline,)
How can I easily do what I want?
You can overwrite the User admin page to display both the Profile and the Property models.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from myapp.models import *
class ProfileInline(admin.TabularInline):
model = Profile
class PropertyInline(admin.TabularInline):
model = Property
class UserAdmin(UserAdmin):
inlines = (ProfileInline, PropertyInline,)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
You can also remove any unwanted/unused User properties from being displayed (e.g. Groups or Permissions)
more here: https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#extending-the-existing-user-model
and here:
https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#a-full-example
class PropertyTabularInline(admin.TabularInline):
model = Property
def formfield_for_dbfield(self, field, **kwargs):
if field.name == 'user':
# implement your method to get userprofile object from request here.
user_profile = self.get_object(kwargs['request'], UserProfile)
kwargs["queryset"] = Property.objects.filter(user=user_profile)
return super(PropertyInLine, self).formfield_for_dbfield(field, **kwargs)
once this is done, you can add this inline to user UserProfileAdmin like:
class UserProfileAdmin(admin.ModelAdmin):
inlines = (PropertyTabularInline,)
Haven't tested it, but that should work.
It is achievable by making one change in your models.
Instead of creating OneToOne relationship from UserProfile to User, subclass User creating UserProfile. Code should look like that:
class UserProfile(User):
# some other fields, no relation to User model
class Property(models.Model):
user = models.ForeignKey(User)
That will result in creating UserProfile model that have hidden OneToOne relation to User model, it won't duplicate user model.
After doing that change, your code will work. There are some changes under the hood, like UserProfile no longer have it's own ID, you can access fields from User inside UserProfile and it's hard to swap User model using settings.AUTH_USER_MODEL (that will require creating some custom function returning proper type and changing migration by hand) but if this is not a problem for you, it may be good solution.

Django: combine two ForeignKeys into one field

I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.

Making a foreign key User model extension in Django required

I am extending the Django User model to include a foreign key pointing at another model like so (just like it says in the Django docs):
models.py:
class Ward(models.Model):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
# Extending the user model
class WardMember(models.Model):
user = models.OneToOneField(User)
ward = models.ForeignKey(Ward)
def __unicode__(self):
return self.ward.name
admin.py:
class WardMemberInline(admin.StackedInline):
model = WardMember
can_delete = False
verbose_name_plural = 'ward member'
# Define a new User admin
class UserAdmin(UserAdmin):
inlines = (WardMemberInline, )
admin.site.register(Ward)
# Re-register UserAdmin to get WardMember customizations
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
When I create a new user in the admin interface I want this new WardMember.ward extension to be required. Currently it's not enforcing that. Here's what happens:
Create user succeeds without a ward
Create other records as user succeed
Edit user now won't let me save unless there is a ward selected
I'd really like #1 above to fail.
I've tried figuring out how to override save() for User using a proxy object but that's not working. I looked into the pre_save signal but the docs explicitly say that's not for vetoing saves.
What is the right approach?
Additional information:
I'm using 1.4. I see that in 1.5 I can extend the user class but I'm not in a position to update to 1.5 just yet.
I ended up forging ahead with Django 1.5, but I'll leave this here in case someone has a final answer to contribute that works with 1.4.
In django 1.3.1 I use this code and works fine:
from django.contrib.auth.models import User
class FilterSearchQueries(models.Model):
title = models.CharField(max_length=250)
owner = models.ForeignKey(User)
place = models.CharField(max_length=250)
query = models.TextField()

Save M2M "Through" Inlines in Django Admin

Apparently Django's ModelAdmin/ModelForm doesn't allow you to use save_m2m() if there's an intermediate through table for a ManyToManyField.
models.py:
from django.db import models
def make_uuid():
import uuid
return uuid.uuid4().hex
class MyModel(models.Model):
id = models.CharField(default=make_uuid, max_length=32, primary_key=True)
title = models.CharField(max_length=32)
many = models.ManyToManyField("RelatedModel", through="RelatedToMyModel")
def save(self, *args, **kwargs):
if not self.id:
self.id = make_uuid()
super(GuidPk, self).save(*args, **kwargs)
class RelatedModel(models.Model):
field = models.CharField(max_length=32)
class RelatedToMyModel(models.Model):
my_model = models.ForeignKey(MyModel)
related_model = models.ForeignKey(RelatedModel)
additional_field = models.CharField(max_length=32)
admin.py:
from django import forms
from django.contrib import admin
from .models import MyModel
class RelatedToMyModelInline(admin.TabularInline):
model = MyModel.many.through
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
class MyModelAdmin(admin.ModelAdmin):
form = MyModelAdminForm
inlines = (RelatedToMyModelInline, )
admin.site.register(MyModel, MyModelAdmin)
If I save MyModel first and then add a new related through model via the inline it works fine, but if I try to set the inline while also adding data for a new MyModel, I get the Django Admin error "Please correct the error below." with nothing highlighted below.
How can I have it save MyModel and then save the inline intermediary models after? Clearly Django can save the through model once it has saved MyModel - so I'm just looking for a hook into that. I tried overriding the form's save() method by calling save_m2m() after calling instance.save(), but apparently that doesn't work for M2Ms with a through table.
I'm using Django 1.2, but this is still an issue in 1.3.
UPDATE: Well, I made a test app like above to isolate the problem, and it appears that it works as expected, correctly saving the M2M intermediary object after saving the MyModel object... as long as I let Django automatically create the MyModel.id field when running python manage.py syncdb - once I added the GUID id field, it no longer works.
This smells more and more like a Django bug.
In your MyModelAdmin you might try overriding the save_formset method. This way you can choose the order in which you save.

Categories

Resources