How to fill test DB in Django with objects of models with ManyToManyFields?

Question:

I want to test speed of my project, so I need to fill my DB with test data.

I have a model with lots of ManyToManyFields (later I will create more models):

class Deciduous(PlantBasicCharacteristics):
    usda_zone = models.ManyToManyField(UsdaZone)
    soil_moisture = models.ManyToManyField(SoilMoisture)
    soil_fertility = models.ManyToManyField(SoilFertility)
    soil_ph = models.ManyToManyField(SoilPh)

And I am trying to create utility to fill DB with test data:

from random import randint

from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models.fields import CharField, DecimalField
from django.db.models.fields.related import ManyToManyField
from django.shortcuts import get_object_or_404

from plants.models import Deciduous
from plants.web_page_filter_fields import get_plant_class_fields

PLANT_CLASSES = [Deciduous, ]
TEST_DATA_AMOUNT = 3


class Command(BaseCommand):

    def handle(self, *args, **options):
        for plant_class in PLANT_CLASSES:
            fields_and_names = get_plant_class_fields(plant_class)
            data = []
            for _ in range(TEST_DATA_AMOUNT):
                data.append(self.create_data(fields_and_names))
            print(data)
            plant_class.objects.bulk_create([
                plant_class(**values) for values in data
            ])

    def create_data(self, fields_and_names):
        data = {}
        for field_name, field_model in fields_and_names.items():
            if ('ptr' not in field_name
                    and 'synonym' not in field_name
                    and field_name != 'id'):
                if isinstance(field_model, CharField):
                    data[field_name] = self.get_string_random()
                elif isinstance(field_model, DecimalField):
                    data[field_name] = self.get_number_random()
                elif isinstance(field_model, ManyToManyField):
                    data[field_name] = [self.get_choice_random(field_model)]
        return data

    def get_string_random(self):
        letters = [chr(randint(97, 122)) for _ in range(randint(5, 20))]
        return ''.join(letters).capitalize()

    def get_number_random(self):
        return randint(10, 15000) / 100

    def get_choice_random(self, model):
        field_model = model.related_model
        field_choices = field_model._meta.get_field('name').choices
        choice_number = randint(0, len(field_choices) - 1)
        choice = field_choices[choice_number][0]
        return get_object_or_404(field_model, name=choice)

But I get:

  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 398, in execute
    output = self.handle(*args, **options)
  File "/app/plants/management/commands/add_test_data.py", line 30, in handle
    plant_class(**values) for values in data
  File "/app/plants/management/commands/add_test_data.py", line 30, in <listcomp>
    plant_class(**values) for values in data
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 498, in __init__
    _setattr(self, prop, kwargs[prop])
  File "/usr/local/lib/python3.7/site-packages/django/db/models/fields/related_descriptors.py", line 547, in __set__
    % self._get_set_deprecation_msg_params(),
TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use usda_zone.set() instead.

Is it possible to create objects using a for loop with set() and not to write code for every ManyToManyField?

Asked By: Inozem

||

Answers:

Try to use the set() method to create objects for ManyToManyFields so:

def create_data(self, fields_and_names):
    data = {}
    for field_name, field_model in fields_and_names.items():
        if ('ptr' not in field_name
                and 'synonym' not in field_name
                and field_name != 'id'):
            if isinstance(field_model, CharField):
                data[field_name] = self.get_string_random()
            elif isinstance(field_model, DecimalField):
                data[field_name] = self.get_number_random()
            elif isinstance(field_model, ManyToManyField):
                choices = field_model.related_model.objects.all()
                data[field_name] = set(choices.order_by('?')[:randint(1, len(choices))])
    return data

Here, order_by('?') is used to randomly order the choices and select a random number of choices using slicing.

Edit

Try this:

from random import sample

# ...

if isinstance(field_model, ManyToManyField):
    choices = field_model.related_model.objects.all()
    count = randint(1, len(choices))
    data[field_name] = set(sample(choices, count))

Edit 2

Try to first create the object without the many-to-many relationships, then add the related objects using the set method so:


def create_data(self, fields_and_names):
    data = {}
    many_to_many_fields = []
    for field_name, field_model in fields_and_names.items():
        if ('ptr' not in field_name
                and 'synonym' not in field_name
                and field_name != 'id'):
            if isinstance(field_model, CharField):
                data[field_name] = self.get_string_random()
            elif isinstance(field_model, DecimalField):
                data[field_name] = self.get_number_random()
            elif isinstance(field_model, ManyToManyField):
                many_to_many_fields.append(field_name)
    obj = Deciduous.objects.create(**data)
    for field_name in many_to_many_fields:
        choices = [self.get_choice_random(Deciduous._meta.get_field(field_name)) for _ in range(randint(1, 3))]
        getattr(obj, field_name).set(choices)
    return data
Answered By: Sunderam Dubey