Auto-generating Data with Django-Migration and Lookup_Field

Nicholas An
4 min readJun 22, 2021

--

As I was making a whole new API and its model table to be used, here came the necessity to make A LOT of data for every single already-existing user accounts. The first thing that came up to my mind was using python manage.py shell to enter some simple scripts with ORMs. However, I felt like I wanted to do something that was more challenging and fun. Plus, this following approach won’t require dev ops or sys admin to manually write ORM script on the dev/production server. They just have to do exactly what they have been doing like any other day in the office.

Overriding `save()` Method to Generate Data in the Future

First, let’s take a look at the case where I made a new table.

class Report(models.Model):
ALL_TABLE_FIELDS = (
'local_ordered_date', 'card', 'country', 'customer', 'supplier',
'amount', 'currency', 'quantity', 'quantity_unit',
... )
DEFAULT_TABLE_FIELDS = {
'OrderTrendReport': ('card', 'board', 'customer', 'amount',
'currency', 'stage', 'owner', 'country',
'local_ordered_date', 'local_delivery_deadline',
'order_number'),
...
}

class ReportType(models.TextChoices):
# hard-coded in order to avoid ImportError and AppRegistryNotReady
OrderTrendReport = 'OrderTrendReport', 'Order Trend report'
RevenueTrendReport = 'RevenueTrendReport', 'Revenue Trend report'
DealSuccessRateReport = 'DealSuccessRateReport', 'Deal Success Rate report'
DealLostReasonReport = 'DealLostReasonReport', 'Deal Lost Reason report'
OrderByCategoryReport = 'OrderByCategoryReport', 'Order By Category report'

user = models.ForeignKey(User, on_delete=models.CASCADE)
report_type = models.CharField(choices=ReportType.choices,
max_length=50,
default=ReportType.OrderTrendReport)
table_fields = ArrayField(models.CharField(max_length=50),
default=list)

@property
def all_table_fields(self):
return self.ALL_TABLE_FIELDS

(I had to shorten some parts for brevity)
As you can see, there is the Report table, within which ReportType, a TextChoices model class, resides in order to support Report’s report_type field. The main idea is that a Report instance should have a report_typefield as a text choice, and a corresponding default table_fields field. Let’s say, there is an instance that has a report_type field as ‘OrderTrendReport’. Let’s take a closer look at table_fields:

table_fields = ArrayField(models.CharField(max_length=50),
default=list)

You can see that the default is already set for list. You can change this to some callable if you have defined such BEFORE you define the value of default. Surely, I tried that like below:

def get_default_table_fields(obj):
return obj.DEFAULT_TABLE_FIELDS[obj.report_type]

However, this will not work because while Django’s default attribute does take a callable as a default, but it DOES NOT take a callable WITH A PARAMETER.

The only way to overcome this predicament would be to override the save() method of the models.Model. Like below:

def save(self, *args, **kwargs):
if not self.pk: # this will ensure that the object is new
self.table_fields = self.DEFAULT_TABLE_FIELDS[self.report_type]
super().save(*args, **kwargs)

Awesome! So what now?

Editing Migration File to Automate Data Generation

It’s time to save lots of instances for every single case of report_type for user accounts. Right now, there are about 5 report_types. So if there are 200 users, there should be 1000 instances of Report. Instead of manually doing this on a shell, you can write additional code to execute while migrating.

import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion


def make_report_fields(apps, schema_editor):
User = apps.get_model(settings.AUTH_USER_MODEL)
Report = apps.get_model('deal', 'Report')
ReportTypes = ('OrderTrendReport', 'RevenueTrendReport', 'DealSuccessRateReport',
'DealLostReasonReport', 'OrderByCategoryReport')
report_fields = [Report(user=user, report_type=report_type)
for report_type in ReportTypes
for user in User.objects.all()]
Report.objects.bulk_create(report_fields)

class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('deal', '0059_auto_20210610_0948'),
]

operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('report_type', models.CharField(choices=[('OrderTrendReport', 'Order Trend report'), ('RevenueTrendReport', 'Revenue Trend report'), ('DealSuccessRateReport', 'Deal Success Rate report'), ('DealLostReasonReport', 'Deal Lost Reason report'), ('OrderByCategoryReport', 'Order By Category report')], default='OrderTrendReport', max_length=50)),
('table_fields', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=50), default=list, size=None)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.RunPython(make_report_fields)
]

However, the above DID NOT WORK!
What happened?

When you take a look at make_report_fields, you can see that I used bulk_create() in order to make a bunch of data at once. Someone on StackOverflow kindly explained that bulk_create() does not use the save() method of the model but instead works on a SQL level. So I tried putting save() method, running a nested for-loop like so:

for user in User.objects.all():
for report_type in ReportTypes:
report = Report(user=user, report_type=report_type)
report.save()

But alas, this still wouldn’t work!

The reason is that the serialized model in the migrations does not have my custom save() method.

So came the last solution: writing the whole dictionary in the migration file.

DEFAULT_TABLE_FIELDS = {
'OrderTrendReport': ('card', 'board', 'customer', 'amount',
'currency', 'stage', 'owner', 'country',
'local_ordered_date', 'local_delivery_deadline',
'order_number'),
'RevenueTrendReport': ('card', 'customer', 'invoice_total',
'payment_total', 'balance', 'currency',
'owner', 'order_number', 'board'),
'DealSuccessRateReport': ('card', 'board', 'customer', 'amount',
'currency', 'owner', 'result',
'local_result_updated_at'),
'DealLostReasonReport': ('card', 'board', 'customer', 'amount',
'currency', 'owner',
'local_result_updated_at', 'lost_reason',
'lost_description'),
'OrderByCategoryReport': ('card', 'board', 'customer', 'amount',
'currency', 'stage', 'owner', 'country',
'local_ordered_date', 'local_delivery_deadline',
'order_number')
}

def make_report_fields(apps, schema_editor):
User = apps.get_model(settings.AUTH_USER_MODEL)
Report = apps.get_model('deal', 'Report')
ReportTypes = ('OrderTrendReport', 'RevenueTrendReport', 'DealSuccessRateReport',
'DealLostReasonReport', 'OrderByCategoryReport')
for user in User.objects.all():
for report_type in ReportTypes:
report = Report(user=user, report_type=report_type,
table_fields=DEFAULT_TABLE_FIELDS[report_type])
report.save()

This guaranteed that each Report instance has correct table_fields recorded.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Nicholas An
Nicholas An

Written by Nicholas An

Backend Server Developer in South Korea

No responses yet

Write a response