Nick An
Nick An

Reputation: 37

Problem Overriding the save() method in Django

I have a peculiar type of model where table_fields needs to set to default depending on the value of its report_type field.

class Report(models.Model):
    ALL_TABLE_FIELDS = (
        'local_ordered_date', 'card', 'country', 'customer', 'supplier',
        'amount', 'currency', 'quantity', 'quantity_unit', 'invoice_total',
        'payment_total', 'balance', 'owner', 'lost_reason', 'lost_description', 'result',
        'local_result_updated_at', 'local_delivery_deadline', 'board', 'stage', 'order_number',
        'product', 'incoterms', 'destination_port', 'loading_port', 'payment_terms',
        'other_terms'
    )
    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')
    }

    """
    The proper way to set a field's default value to a function call/callable
    is to declare a function before the field and use it as a callable in default_value named arg
    https://stackoverflow.com/questions/12649659/how-to-set-a-django-model-fields-default-value-to-a-function-call-callable-e
    """

    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

    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)

After finishing the code for models.py, I went on and made a migration file. Then I edited this like so:

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)
    ]

When I ran python migrate deal {# of migration file}, the table was made, with many rows corresponding to the existing users. However, the table_fields were all empty.

empty_table_fields

How do I fix it so that the table_fields will be set to correct default according to its 'report_type'?

Upvotes: 0

Views: 278

Answers (1)

Abdul Aziz Barkat
Abdul Aziz Barkat

Reputation: 21812

As described in the documentation one of the caveats of bulk_create is as follows:

The model’s save() method will not be called, and the pre_save and post_save signals will not be sent.

Also custom model methods are not available in the migrations as they use a serialized representation of your models without them. Hence your code never uses the save method causing your code to not work as you want it to. Instead of relying on the save method why don't you simply pass the correct array yourself? Something like follows:

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')
    report_fields = [Report(user=user, report_type=report_type, table_fields=DEFAULT_TABLE_FIELDS[report_type])
                     for report_type in ReportTypes
                     for user in User.objects.all()]
    Report.objects.bulk_create(report_fields)

Upvotes: 1

Related Questions