Django edit data error when model has some related fields

Feature(s) impacted

Forest admin edit ui

Observed behavior

Got an error when editing (company) data that has a related field in model

An error occured when trying to edit Company : Direct assignment to the reverse side of a related set is prohibited. Use contacts.set() instead.

And model are like :

class Company(models.Model):
    name = models.CharField(max_length=250, unique=True)
    qualification_status = models.CharField(
        max_length=250,
        choices=enum_to_list_map(QUALIFICATION_STATUS),
        default=QUALIFICATION_STATUS.UNKNOW.value,
    )
    company_type = models.CharField(
        max_length=250,
        choices=enum_to_list_map(COMPANY_TYPE),
        default=COMPANY_TYPE.UNKNOW.value,
    )
    consolidation_status = models.CharField(
        max_length=250,
        choices=enum_to_list_map(CONSOLIDATION_STATUS),
        default=CONSOLIDATION_STATUS.NO_CONSOLIDATION_REQUIRED.value,
    )
    prospecting_status = models.CharField(
        max_length=250,
        choices=enum_to_list_map(PROSPECTING_STATUS),
        default=PROSPECTING_STATUS.INACTIF.value,
    )

    domain = models.CharField(max_length=250, null=True, blank=True)
    website = models.CharField(max_length=250, null=True, blank=True)
    website_meta_description = models.CharField(max_length=250, null=True, blank=True)
    website_content = models.CharField(max_length=5000, null=True, blank=True)

    linkedin_page = models.CharField(max_length=500, null=True, blank=True)

    address = models.CharField(max_length=250, null=True, blank=True)
    city = models.CharField(max_length=250, null=True, blank=True)
    postal_code = models.CharField(max_length=250, null=True, blank=True)
    country = models.CharField(max_length=250, null=True, blank=True)

    naf = models.CharField(max_length=250, null=True, blank=True)
    naf_des = models.CharField(max_length=250, null=True, blank=True)
    siren = models.CharField(max_length=250, null=True, blank=True)
    siret = models.CharField(max_length=250, null=True, blank=True)
    vat_number = models.CharField(max_length=250, null=True, blank=True)

    staff_count = models.CharField(max_length=250, null=True, blank=True)
    staff_interval = models.CharField(max_length=250, null=True, blank=True)

    not_interesting_reason = models.CharField(max_length=250, null=True, blank=True)
    score = models.IntegerField(default=0)
    score_last_update = models.DateTimeField(null=True, blank=True)
    _has_cto_in_contacts = models.BooleanField(default=False)
    _has_devops_in_contacts = models.BooleanField(default=False)

class Contact(models.Model):
    firstname = models.CharField(max_length=250, null=True, blank=True)
    lastname = models.CharField(max_length=250, null=True, blank=True)
    email = models.CharField(max_length=250, null=True, blank=True)
    job_title = models.CharField(max_length=250, null=True, blank=True)
    other_emails = models.CharField(max_length=250, null=True, blank=True)
    linkedin = models.CharField(max_length=250, null=True, blank=True)
    phones = models.CharField(max_length=250, null=True, blank=True)
    company = models.ForeignKey(
        Company, related_name='contacts', on_delete=models.CASCADE, null=True, blank=True
    )
    consolidation_status = models.CharField(
        max_length=250, choices=enum_to_list_map(CONSOLIDATION_STATUS)
    )

Expected behavior

Should edit the company field properly

Failure Logs

Frontend logs

An error occured when trying to edit Company : Direct assignment to the reverse side of a related set is prohibited. Use contacts.set() instead.

Backend logs

Bad Request: /forest/core_company/2276

Context

  • Project name: Keltio Intent
  • Team name: Keltio
  • Environment name: Production
  • Agent (forest package) name & version: django-forestadmin==1.4.11
  • Database type: Mysql
  • Recent changes made on your end if any: Added blank to all model attributes because otherwise i must provide all the data to save data for a specific company

Don’t know if it can help to fix the problem but in the .forestadmin-schema.json file the field contacts is like this :

        {
          "field": "contacts",
          "type": [
            "Number"
          ],
          "is_filterable": false,
          "is_sortable": true,
          "is_read_only": false,
          "is_required": false,
          "is_virtual": false,
          "default_value": null,
          "integration": null,
          "reference": "core_contact.id",
          "inverse_of": "company",
          "relationship": "HasMany",
          "widget": null
        },

Does it change something if i put the value is_read_only to true ? (i know that we must not edit this file but if it fix my problem this can be a workaround)

Note :
It does not change anything to set is_read_only to true

Here is the payload sent by the frontend to the backend :

json_data = {
    'data': {
        'id': '2276',
        'attributes': {
            'qualification_status': 'CLIENT',
        },
        'relationships': {
            'contacts': {
                'data': [],
            },
            'welcome_to_the_jungle_hiring_intents': {
                'data': [
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '3695',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '5066',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '5024',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '5023',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4940',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4575',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4232',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4029',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '3976',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '3690',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4240',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4359',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4383',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4179',
                    },
                    {
                        'type': 'core_welcometothejunglehiringintents',
                        'id': '4178',
                    },
                ],
            },
            'indeed_hiring_intents': {
                'data': [
                    {
                        'type': 'core_indeedhiringintents',
                        'id': '8087',
                    },
                ],
            },
            'linkedin_hiring_intents': {
                'data': [
                    {
                        'type': 'core_linkedinhiringintents',
                        'id': '279',
                    },
                ],
            },
            'jaimelesstartup_fundrasing_intents': {
                'data': [],
            },
        },
        'type': 'core_companies',
    },
}

Ok i found a way to fix the bug by ovewriting the url like this :

from django_forest.resources.views.detail import DetailView
from django_forest.utils.schema.json_api_schema import JsonApiSchema
from django_forest.resources.utils.resource import ResourceView
from core.models import Company

class CompanyCountView(ResourceView):
    def dispatch(self, request, *args, **kwargs):
        try:
            self.Model = Company
        except Exception as e:
            return self.error_response(e)
        else:
            return super().dispatch(request, "core_company", *args, **kwargs)

    def get(self, request):
        queryset = self.Model.objects.all()
        params = request.GET.dict()
        return self.get_count(queryset, params, request)


class CompanyDetailView(DetailView):
    def dispatch(self, request, *args, **kwargs):
        try:
            self.Model = Company
        except Exception as e:
            return self.error_response(e)
        else:
            return super().dispatch(request, "core_company", *args, **kwargs)

    def put(self, request, pk):
        body = self.get_body(request.body)

        try:
            attributes = self.populate_attribute(body)
            if "contacts" in attributes:
                del attributes["contacts"]
            if "welcome_to_the_jungle_hiring_intents" in attributes:
                del attributes["welcome_to_the_jungle_hiring_intents"]
            if "indeed_hiring_intents" in attributes:
                del attributes["indeed_hiring_intents"]
            if "linkedin_hiring_intents" in attributes:
                del attributes["linkedin_hiring_intents"]
            if "jaimelesstartup_fundrasing_intents" in attributes:
                del attributes["jaimelesstartup_fundrasing_intents"]

            instance = self.get_instance(request, pk)
            for k, v in attributes.items():
                setattr(instance, k, v)
            instance = self.update_smart_fields(instance, body, self.Model._meta.db_table)
            instance.save()
        except Exception as e:
            return self.error_response(e)
        else:
            # Notice: one to one case, where a new object is created with a new pk
            # It needs to be deleted, as django orm will create a new object
            if str(instance.pk) != pk:
                self.Model.objects.filter(pk=pk).delete()

            # json api serializer
            Schema = JsonApiSchema._registry[f'{self.Model._meta.db_table}Schema']
            data = Schema().dump(instance)
            return JsonResponse(data, safe=False)

In urls file :

from core.views.company_website import CompanyWebsiteView
from core.views.company import CompanyDetailView, CompanyCountView

app_name = 'core'
urlpatterns = [
    path('/core_companywebsite', csrf_exempt(CompanyWebsiteView.as_view()), name='core_companywebsite'),
    path('/core_company/count', CompanyCountView.as_view(), name='core_companycount'),
    path('/core_company/<pk>', csrf_exempt(CompanyDetailView.as_view()), name='core_companydetail'),
...
]

Hello,
Your error is very weird, you are not to supposed to receive the contacts attributes because it is a related data.
When you are editing your company from the forestadmin UI, do you see the contacts attribute in your form ?

Nope i don’t see the contacts in the form

Indeed this behavior is weird but after making the fix above it works