Django : Migration of polymorphic models back to a single base class - python

Let's suppose I have a polymorphic model and I want to get rid of it.
class AnswerBase(models.Model):
question = models.ForeignKey(Question, related_name="answers")
response = models.ForeignKey(Response, related_name="answers")
class AnswerText(AnswerBase):
body = models.TextField(blank=True, null=True)
class AnswerInteger(AnswerBase):
body = models.IntegerField(blank=True, null=True)
When I want to get all the answers I can never access "body" or I need to try to get the instance of a sub-class by trial and error.
# Query set of answerBase, no access to body
AnswerBase.objects.all()
question = Question.objects.get(pk=1)
# Query set of answerBase, no access to body (even with django-polymorphic)
question.answers.all()
I don't want to use django-polymorphic because of performances, because it does not seem to work for foreignKey relation, and because I don't want my model to be too complicated. So I want this polymorphic architecture to become this simplified one :
class Answer(models.Model):
question = models.ForeignKey(Question, related_name="answers")
response = models.ForeignKey(Response, related_name="answers")
body = models.TextField(blank=True, null=True)
The migrations cannot be created automatically, it would delete all older answers in the database. I've read the Schema Editor documentation but it does not seem there is a buildin to migrate a model to something that already exists. So I want to create my own operation to save the AnswerText and AnswerInteger as an Answer then delete AnswerText and AnswerInteger. I'm hoping I won't have to write SQL directly, but maybe that's the only solution ? My migration file looks like this. I created an Operation called MigrateAnswer :
from myapp.migrations import MigrateAnswer
class Migration(migrations.Migration):
operations = [
migrations.RenameModel("AnswerBase", "Answer"),
migrations.AddField(
model_name='answer',
name='body',
field=models.TextField(blank=True, null=True),
),
MigrateAnswer("AnswerInteger"),
MigrateAnswer("AnswerText"),
migrations.DeleteModel(name='AnswerInteger',),
migrations.DeleteModel(name='AnswerText',),
]
So what I want to do in MigrateAnswer is to migrate the value for an old model (AnswerInteger and AnswerText) to the base class (now named Answer, previousely AnswerBase). Here's my operation class :
from django.db.migrations.operations.base import Operation
class MigrateAnswer(Operation):
reversible = False
def __init__(self, model_name):
self.old_name = model_name
def database_forwards(self, app_label, schema_editor, from_state,
to_state):
new_model = to_state.apps.get_model(app_label, "Answer")
old_model = from_state.apps.get_model(app_label, self.old_name)
for field in old_model._meta.local_fields:
# loop on "question", "reponse" and "body"
# schema_editor.alter_field() Alter a field on a single model
# schema_editor.add_field() + remove_field() Does not permit
# to migrate the value from the old field to the new one
pass
So my question is : Is it possible to do this wihout using "execute" (ie : without writing SQL). If so what should I do in the for loop of my Operation ?
Thanks in advance !

There is no need to write an Operations class; data migrations can be done simply with a RunPython call, as the docs show.
Within that function you can use perfectly normal model instance methods; since you know the fields you want to move the data for, there is no need to get them via meta lookups.
However you will need to temporarily call the new body field a different name, so it doesn't conflict with the old fields on the subclasses; you can rename it back at the end and delete the child classes because the value will be in the base class.
def migrate_answers(apps, schema_editor):
classes = []
classes_str = ['AnswerText', 'AnswerInteger']
for class_name in classes_str:
classes.append(apps.get_model('survey', class_name))
for class_ in classes:
for answer in class_.objects.all():
answer.new_body = answer.body
answer.save()
operations = [
migrations.AddField(
model_name='answerbase',
name='new_body',
field=models.TextField(blank=True, null=True),
),
migrations.RunPython(migrate_answers),
migrations.DeleteModel(name='AnswerInteger',),
migrations.DeleteModel(name='AnswerText',),
migrations.RenameField('AnswerBase', 'new_body', 'body'),
migrations.RenameModel("AnswerBase", "Answer"),
]

You could create an empty migration for the app you want to do these modifications and use the migrations.RunPython Class to execute custom python functions.
Inside these functions you can have access to your models
The Django ORM that you can do data manipulation.
Pure python, no raw SQL.

Related

Stop affecting other objects of Django Many to Many model

I'm trying to replicate Blood Group as Model as defined in this picture.
.
In my models.py file I had my code to replicate the blood groups like this
class BloodGroup(models.Model):
name = models.CharField(
max_length=3
)
gives = models.ManyToManyField("self")
receives = models.ManyToManyField("self")
def __str__(self):
return self.name
And in my admin.py file I had registered the model as follows
class BloodGroupAdmin(admin.ModelAdmin):
model = BloodGroup
list_display = ['name', 'get_gives', 'get_receives']
def get_gives(self, obj):
return ", ".join([item.name for item in obj.gives.all()])
def get_receives(self, obj):
return ", ".join([item.name for item in obj.receives.all()])
admin.site.register(BloodGroup, BloodGroupAdmin)
Initially I created plain BloodGroup objects without their gives and receives attribute by providing just their names alone. Thus I create an object for all 8 types. Then as I added relationships to each object I found that adding gives or receives for one object affects other objects gives and receives too, making it impossible to replicate the structure in image.
How do I define relationships, without affecting other objects?
In my admin site, I see field names as "get_gives" and "get_receives". How would i make the admin page show field names as "gives" and "receives" but still displaying objects as strings like the image below?
For first question, probably it is better to have only one relation gives. receives can be found from the reverse query. Like this:
class BloodGroup(models.Model):
name = models.CharField(
max_length=3
)
gives = models.ManyToManyField("self", related_name="receives", symmetrical=False)
Then you only need to add objects to gives. receives will be generated automatically.
For second question, add short_description attribute to function(reference to docs). Like this:
get_gives.short_description = 'Gives'
get_receives.short_description = 'Receives'

Django Rest Framework updating nested m2m objects. Does anyone know a better way?

I have a case when user needs to update one instance together with adding/editing the m2m related objects on this instance.
Here is my solution:
# models.py
class AdditionalAction(SoftDeletionModel):
ADDITIONAL_CHOICES = (
('to_bring', 'To bring'),
('to_prepare', 'To prepare'),
)
title = models.CharField(max_length=50)
type = models.CharField(choices=ADDITIONAL_CHOICES, max_length=30)
class Event(models.Model):
title= models.CharField(max_length=255)
actions = models.ManyToManyField(AdditionalAction, blank=True)
# serializers.py
class MySerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
actions_data = validated_data.pop('actions')
# Use atomic block to rollback if anything raised Exception
with transaction.atomic():
# update main object
updated_instance = super().update(instance, validated_data)
actions = []
# Loop over m2m relation data and
# create/update each action instance based on id present
for action_data in actions_data:
action_kwargs = {
'data': action_data
}
id = action_data.get('id', False)
if id:
action_kwargs['instance'] = AdditionalAction.objects.get(id=id)
actions_ser = ActionSerializerWrite(**action_kwargs)
actions_ser.is_valid(raise_exception=True)
actions.append(actions_ser.save())
updated_instance.actions.set(actions)
return updated_instance
Can anyone suggest better solution?
P.S. actions can be created or updated in this case, so i can't just use many=True on serializer cause it also needs instance to update.
Using for loop with save here will be a killer if you have a long list or actions triggered on save, etc. I'd try to avoid it.
You may be better off using ORMS update with where clause: https://docs.djangoproject.com/en/2.0/topics/db/queries/#updating-multiple-objects-at-once and even reading the updated objects from the database after the write.
For creating new actions you could use bulk_create:https://docs.djangoproject.com/en/2.0/ref/models/querysets/#bulk-create
There is also this one: https://github.com/aykut/django-bulk-update (disclaimer: I am not a contributor or author of the package).
You have to be aware of cons of this method - if you use any post/pre_ save signals those will not be triggered by the update.
In general, running multiple saves will kill the database, and you might end up with hard to diagnose deadlocks. In one of the projects I worked on moving from save() in the loop into update() decreased response time from 30 something seconds to < 10 where the longest operations left where sending emails.

Django Grapelli add autocomplete lookups on InlineAdmin

I have this 3 models:
class MyFile(models.Model):
file = models.FileField(upload_to="files/%Y/%m/%d")
def __unicode__(self):
"""."""
return "%s" % (
self.file.name)
class ExampleModel(models.Model):
attached_files =models.ManyToManyField(MyFile)
main_model = models.ForeignKey(MainModel)
class MainModel(models.Model):
attached_files =models.ManyToManyField(MyFile)
And my admin.py as follows:
class ExampleModelAdminInline(admin.TabularInline):
model = ExampleModel
extra = 2
class MainModelAdmin(admin.ModelAdmin):
inlines = [ExampleModelAdminInline]
Im using django-grapelli because it offer autocomplete lookups for many to many fields. However, Im not sure how to add this autocomplete lookup to a TabularInline admin. Can anyone explain me how to set up the attached_files field to have autocomplete lookups?
First you need to set the static method autocomplete_search_fields() in the Model you want to search from, in your case MyFile. From the docs we get:
class MyFile(models.Model):
#your variable declaration...
#staticmethod
def autocomplete_search_fields():
return ("id__iexact", "name__icontains",) #the fields you want here
You can also define GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS instead of declaring the static method, like:
GRAPPELLI_AUTOCOMPLETE_SEARCH_FIELDS = {
"myapp": {
"MyFile": ("id__iexact", "name__icontains",)
}
}
Then you should add the lookup and raw fields to your desired admin class, considering its related Model (say, your ExampleModel) which is the one that has a ManyToManyField. You can also handle ForeignKey in a similar way. Also from the mentioned docs:
class ExampleModel(models.Model):
main_model = models.ForeignKey(MainModel) #some FK to other Model related
attached_files =models.ManyToManyField(MyFile) #the one with static decl
class MainModelAdmin(admin.ModelAdmin):
#your variable declaration...
# define raw fields
raw_id_fields = ('main_model','attached_files',)
# define the autocomplete_lookup_fields
autocomplete_lookup_fields = {
'fk': ['main_model'],
'm2m': ['attached_files'],
}
Remember to register both ends (your models) of the relationship to your admin.site, like this:
#the one with the m2m and the one with the lookup
admin.site.register(ExampleModel, MainModelAdmin)
You can also check this question to understand better.

django-rest-framework 3.0 create or update in nested serializer

With django-rest-framework 3.0 and having these simple models:
class Book(models.Model):
title = models.CharField(max_length=50)
class Page(models.Model):
book = models.ForeignKey(Books, related_name='related_book')
text = models.CharField(max_length=500)
And given this JSON request:
{
"book_id":1,
"pages":[
{
"page_id":2,
"text":"loremipsum"
},
{
"page_id":4,
"text":"loremipsum"
}
]
}
How can I write a nested serializer to process this JSON and for each page for the given book either create a new page or update if it exists.
class RequestSerializer(serializers.Serializer):
book_id = serializers.IntegerField()
page = PageSerializer(many=True)
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
I know that instantiating the serializer with an instance will update the current one but how should I use it inside the create method of nested serializer?
Firstly, do you want to support creating new book instances, or only updating existing ones?
If you only ever wanted to create new book instances you could do something like this...
class PageSerializer(serializers.Serializer):
text = serializers.CharField(max_length=500)
class BookSerializer(serializers.Serializer):
page = PageSerializer(many=True)
title = serializers.CharField(max_length=50)
def create(self, validated_data):
# Create the book instance
book = Book.objects.create(title=validated_data['title'])
# Create or update each page instance
for item in validated_data['pages']:
page = Page(id=item['page_id'], text=item['text'], book=book)
page.save()
return book
Note that I haven't included the book_id here. When we're creating book instances we won't be including a book id. When we're updating book instances we'll typically include the book id as part of the URL, rather than in the request data.
If you want to support both create and update of book instances then you need to think about how you want to handle pages that are not included in the request, but are currently associated with the book instance.
You might choose to silently ignore those pages and leave them as they are, you might want to raise a validation error, or you might want to delete them.
Let's assume that you want to delete any pages not included in the request.
def create(self, validated_data):
# As before.
...
def update(self, instance, validated_data):
# Update the book instance
instance.title = validated_data['title']
instance.save()
# Delete any pages not included in the request
page_ids = [item['page_id'] for item in validated_data['pages']]
for page in instance.books:
if page.id not in page_ids:
page.delete()
# Create or update page instances that are in the request
for item in validated_data['pages']:
page = Page(id=item['page_id'], text=item['text'], book=instance)
page.save()
return instance
It's also possible that you might want to only support book updates, and not support creation, in which case, only include the update() method.
There are also various ways you could reduce the number of queries eg. using bulk create/deletion, but the above would do the job in a fairly straightforward way.
As you can see there are subtleties in the types of behavior you might want when dealing with nested data, so think carefully about exactly what behavior you're expecting in various cases.
Also note that I've been using Serializer in the above example rather than ModelSerializer. In this case it's simpler just to include all the fields in the serializer class explicitly, rather than relying on the automatic set of fields that ModelSerializer generates by default.
You can simply use drf-writable-nested.
It automatically make your nested serializers writable and updatable.
in you serializers.py:
from drf_writable_nested import WritableNestedModelSerializer
class RequestSerializer(WritableNestedModelSerializer):
book_id = serializers.IntegerField()
page = PageSerializer(many=True)
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
And that's it!
Also the library supports using only one of the create and update logics if you don't need both.

How to post to a Django REST Framework API with Related Models

I have two related models (Events + Locations) with a serialzer shown below:
class Locations
title = models.CharField(max_length=250)
address = model.CharField(max_length=250)
class Events
title = models.CharField(max_length=250)
locations = models.ForeignKey(Locations, related_name='events'
class EventsSerializer(serializers.ModelSerializer):
class Meta:
model = Events
depth = 1
I set the depth to 1 in the serializer so I can get the information from the Locations model instead of a single id. When doing this however, I cant post to events with the location info. I can only perform a post with the title attribute. If I remove the depth option in the serializer, I can perform the post with both the title and location id.
I tried to create a second serializer (EventsSerialzerB) without the depth field with the intention of using the first one as a read-only response, however when I created a second serializer, viewset, and added it to the router, it would automatically override the original viewset.
Is it possible for me to create a serializer that outputs the related model fields, and allows you to post directly to the single model?
// EDIT - Here's what I'm trying to post
$scope.doClick = function (event) {
var test_data = {
title: 'Event Test',
content: 'Some test content here',
location: 2,
date: '2014-12-16T11:00:00Z'
}
// $resource.save() doesn't work?
$http.post('/api/events/', test_data).
success(function(data, status, headers, config) {
console.log('sucess', status);
}).
error(function(data, status, headers, config) {
console.log('error', status);
});
}
So when the serializers are flat, I can post all of these fields. The location field is the id of a location from the related Locations table. When they are nested, I can't include the location field in the test data.
By setting the depth option on the serializer, you are telling it to make any relation nested instead of flat. For the most part, nested serializers should be considered read-only by default, as they are buggy in Django REST Framework 2.4 and there are better ways to handle them in 3.0.
It sounds like you want a nested representation when reading, but a flat representation when writing. While this isn't recommended, as it means GET requests don't match PUT requests, it is possible to do this in a way to makes everyone happy.
In Django REST Framework 3.0, you can try the following to get what you want:
class LocationsSerializer(serializers.ModelSerializer):
class Meta:
model = Locations
fields = ('title', 'address', )
class EventsSerializer(serializers.ModelSerializer):
locations = LocationsSerializer(read_only=True)
class Meta:
model = Events
fields = ('locations', )
class EventViewSet(viewsets.ModelViewSet):
queryet = Event.objects.all()
serializer_class = EventsSerializer
def perform_create(self, serializer):
serializer.save(locations=self.request.data['locations'])
def perform_update(self, serializer):
serializer.save(locations=self.request.data['locations'])
A new LocationsSerializer was created, which will handle the read-only nested representation of the Locations object. By overriding perform_create and perform_update, we can pass in the location id that was passed in with the request body, so the location can still be updated.
Also, you should avoid having model names being plurals. It's confusing when Events.locations is a single location, even though Locations.events is a list of events for the location. Event.location and Location.events reads a bit more clearly, the Django admin will display them reasonably, and your fellow developers will be able to easily understand how the relations are set up.

Categories

Resources