Saving related model objects - django

I have two related models (one to many) in my django app and When I do something like this
ObjBlog = Blog()
objBlog.name = 'test blog'
objEntry1 = Entry()
objEntry1.title = 'Entry one'
objEntry2 = Entry()
objEntry2.title = 'Entry Two'
objBlog.entry_set.add(objEntry1)
objBlog.entry_set.add(objEntry2)
I get an error which says "null value in column and it violates the foreign key not null constraint".
None of my model objects have been saved. Do I have to save the "objBlog" before I could set the entries? I was hoping I could call the save method on objBlog to save it all.
NOTE: I am not creating a blog engine and this is just an example.

I would guess that one of your models has a Foreign Key field which is not nullable.
When you do objBlog.entry_set.add(objEntry1) django calls save() on each object.
This is how the add method looks like:
def add(self, *objs):
for obj in objs:
if not isinstance(obj, self.model):
raise TypeError("'%s' instance expected" % self.model._meta.object_name)
setattr(obj, rel_field.name, instance)
obj.save()
add.alters_data = True

Related

Django JSONField not receiving cleaned data

I have a very simple model which contains a JSONField:
class Thing(models.Model):
title = models.CharField(max_length=1024)
text = JSONField(default=dict)
I've created a custom widget that allows for the input of key-value pairs:
class JsonWidget(forms.widgets.Widget):
template_name = 'json_widget.html'
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
data = json.loads(value)
if not data:
data = JSON_DEFAULT
context['data'] = data.items()
return context
def value_from_datadict(self, data, files, name):
keys = data.getlist('json-key')
values = data.getlist('json-value')
json_data = {k: v for k, v in zip(keys, values)}
return json_data
I coerce the dict returned by the widget into a string in the field's clean function on the form:
class ThingForm(forms.ModelForm):
class Meta:
model = Thing
fields = ['title', 'text']
widgets = {
'text': JsonWidget(),
}
def clean_text(self):
text = self.cleaned_data.get('text')
return json.dumps(text)
I've inspected the output of JsonWidget.value_from_datadict (dict) and ThingForm.clean_text (str) are the expected types. But when the object goes to save it throws an exception:
TypeError: the JSON object must be str, bytes or bytearray, not 'dict'
This is my first time building a custom widget for Django 1.11, is there something obvious I've missed here?
Thanks!
This was a tricky one but I eventually traced the problem back to Instance construction fails on form data check
While the JSONField is called text on the model, there's no matching field on the ModelForm. Instead, the key/value pairs are compiled & turned into a dict by the widget's value_from_datadict. However, the cleaned_data values are saved back to the instance only if the field name exists in the form's POST data. This caused the widget to throw an error because text could not be blank, and the TypeError exception was raised during the form re-render.
The workaround was to add a hidden input called text to the field and only use it to bypass the field name check during instantion.

OneToOneField Relationship Not Created

I have a form in which I take the data from some of the fields and create a new model object, then assign that newly created object a one-to-one relationship to a preexisting object of a different model. Here is my save method in the form.
def save(self, *args, **kwargs):
super(CustomerProfileForm, self).save(*args, **kwargs)
if self.cleaned_data['street_address']:
if not self.instance.customer.home_location:
home_location = Location()
else :
home_location = self.instance.customer.home_location
home_location.name = 'Home/Apartment'
home_location.street_address = self.cleaned_data['street_address']
home_location.city = self.cleaned_data['city']
home_location.state = self.cleaned_data['state']
home_location.zip_code = self.cleaned_data['zip_code']
self.instance.customer.home_location = home_location
home_location.save()
self.instance.customer.save()
return self.instance
The Location object is being created and populated with the information from the form as I expect, but the OneToOne relationship with the CustomerProfile object (self.instance) is not being assigned. Does anyone know why this might be?
This makes no sense to me. When I print self.instance.customer.home_location right before the end of the save function, the new location is logged to the console, which shows that the relationship is assigned... How does it get unassigned after the save method completes...?
In order to save a relationship, the object needs to have a primary key; and this is only generated after the object is saved.
Therefore, you need to save the object first, before assigning it as a foreign key:
home_location.save()
self.instance.customer.home_location = home_location
# home_location.save() - this line should come before any relationships
# are linked to the object.
self.instance.customer.save()

Django loop over all m2m relations

I need to loop over all m2m relations of the model instance and copy them to new model instance.
source_id=request.GET.get('source_id', 1)
obj = Artist.objects.create(title='New artist')
source_obj = Artist.objects.get(id=source_id)
if source_obj.galleries.count():
obj.galleries = source_obj.galleries.all()
if source_obj.suggested_artists.count():
obj.suggested_artists = source_obj.suggested_artists.all()
Currently i am doing it like this, but i want to loop over all m2m fields and copy the related data to obj.
I want something like:
for m2m_rel in source_obj.m2m_relations:
print geattr(source_obj, m2m_rel).count()
print geattr(source_obj, m2m_rel).all()
Any suggestions?
You can access the m2m relation entries like this:
for field in source_obj._meta.many_to_many:
source = getattr(source_obj, field.attname)
for item in source.all():
# do something with item...
print repr(item)
If you're trying to clone a model intance, you can use a generic clone_objects function like the one below. The function will clone a list of objects and return a new list of cloned objects (with new ID's):
# import Python's copy library
import copy
def clone_objects(objects):
"""
Generic model object cloner function.
"""
def clone(obj):
"""Return an identical copy of the instance with a new ID."""
if not obj.pk:
raise ValueError('Instance must be saved before it can be cloned.')
duplicate = copy.copy(obj)
# Setting pk to None tricks Django into thinking this is a new object.
duplicate.pk = None
duplicate.save()
# ... but the trick loses all ManyToMany relations.
for field in obj._meta.many_to_many:
source = getattr(obj, field.attname)
destination = getattr(duplicate, field.attname)
for item in source.all():
destination.add(item)
return duplicate
if not hasattr(objects,'__iter__'):
objects = [ objects ]
objs = []
for obj in objects:
new_obj = clone(obj)
new_obj.save()
objs.append(new_obj)
return objs
The main part of the "cloning" code is from this snippet: Clone model mixin

django ManyToManyField and on_delete

ForeignKeys on django have the attribute on_delete to specify the behavior when the referenced object is deleted. Is there any way to get something similar for ManyToManyField?
Suppose I have the following model
class House(models.Model):
owners = models.ManyToManyField(Person)
The default behavior is to cascade, so if I delete a person that happens to own a house, it just vanishes from owners (that is, obviously, it no longer owns any houses). What I'd like to have is that if a person is an owner, it can not be deleted. That is, I want on_delete=models.PROTECT. Is this possible?
I know internally ManyToManyField is translated to another model with two ForeignKeys (in this case one to house and one to person), so it should be possible to achieve this. Any ideas how to? I'd like to avoid setting the through attribute to a new model, because this would result in a new table (I'd like to keep the old one).
Edit: I've tracked where django creates the appropriate m2m model:
def create_many_to_many_intermediary_model(field, klass):
from django.db import models
# ...
# Construct and return the new class.
return type(name, (models.Model,), {
'Meta': meta,
'__module__': klass.__module__,
from_: models.ForeignKey(klass,
related_name='%s+' % name,
db_tablespace=field.db_tablespace),
to: models.ForeignKey(to_model,
related_name='%s+' % name,
db_tablespace=field.db_tablespace)
})
The relevant line is
to: models.ForeignKey(to_model,
related_name='%s+' % name,
db_tablespace=field.db_tablespace)
I'd like it to be
to: models.ForeignKey(to_model,
related_name='%s+' % name,
db_tablespace=field.db_tablespace,
on_delete=models.PROTECT)
Any way to do this other than monkey patching the whole thing and creating a new class for ManyToManyField?
I think the smartest thing to do is use an explicit through table. I realise that you've stated you would prefer not to "because this would result in a new table (I'd like to keep the old one)."
I suspect your concern is over losing the data you have. If you're using South, you can easily "convert" your existing, automatic intermediate table to an explicit one OR, you can create a completely new one, then migrate your existing data to the new table before dropping your old one.
Both of these methods are explained here: Adding a "through" table to django field and migrating with South?
Considering the change you'd like to make to its definition, I'd probably go with the option of creating a new table, then migrating your data over. Test to make sure all your data is still there (and that your change does what you want), then drop the old intermediate table.
Considering that these tables will both only hold 3 integers per row, this is likely to be a very manageable exercise even if you have a lot of houses and owners.
If I understand you want, this is similar to what I need some time ago.
Your problem: you need to protect a record that is used in another table from accidental deletion.
I solved it from this way (tested on Django 2 and Django 3).
Imagine, you have:
TABLE1 and TABLE 2, and they are under M2M relationship where TABLE1 has ManyToManyField.
I put the main keys to you understand at uppercase, you will need to adjust to what you want.
Look at views.py that use the exists() method and rise the exception are crucial.
models.py
class TABLE1(models.Model):
FIELD_M2M = models.ManyToManyField(
TABLE2,
blank=False,
related_name='FIELD_M2M',
)
#put here your code
models.py
class TABLE2(models.Model):
#Put here your code
views.py
# Delete
#login_required
def delete(request, pk=None):
try: # Delete register selected
if TABLE1.objects.filter(FIELD_M2M=pk).exists():
raise IntegrityError
register_to_delete = get_object_or_404(TABLE2, pk=pk)
# register_to_delete.register_to_delete.clear() // Uncomment this, if you need broken relationship M2M before delete
register_to_delete.delete()
except IntegrityError:
message = "The register couldn't be deleted!"
messages.info(request, message)
That is a ugly solution, but it works.
Posting my own solution as requested by #Andrew Fount. Quite an ugly hack just to change a single line.
from django.db.models import ManyToManyField
from django.db.models.fields.related import ReverseManyRelatedObjectsDescriptor, add_lazy_relation, create_many_to_many_intermediary_model, RECURSIVE_RELATIONSHIP_CONSTANT
from django.utils import six
from django.utils.functional import curry
def create_many_to_many_protected_intermediary_model(field, klass):
from django.db import models
managed = True
if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
to_model = field.rel.to
to = to_model.split('.')[-1]
def set_managed(field, model, cls):
field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
add_lazy_relation(klass, field, to_model, set_managed)
elif isinstance(field.rel.to, six.string_types):
to = klass._meta.object_name
to_model = klass
managed = klass._meta.managed
else:
to = field.rel.to._meta.object_name
to_model = field.rel.to
managed = klass._meta.managed or to_model._meta.managed
name = '%s_%s' % (klass._meta.object_name, field.name)
if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name:
from_ = 'from_%s' % to.lower()
to = 'to_%s' % to.lower()
else:
from_ = klass._meta.object_name.lower()
to = to.lower()
meta = type('Meta', (object,), {
'db_table': field._get_m2m_db_table(klass._meta),
'managed': managed,
'auto_created': klass,
'app_label': klass._meta.app_label,
'db_tablespace': klass._meta.db_tablespace,
'unique_together': (from_, to),
'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to},
'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to},
})
# Construct and return the new class.
return type(name, (models.Model,), {
'Meta': meta,
'__module__': klass.__module__,
from_: models.ForeignKey(klass, related_name='%s+' % name, db_tablespace=field.db_tablespace),
### THIS IS THE ONLY LINE CHANGED
to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace, on_delete=models.PROTECT)
### END OF THIS IS THE ONLY LINE CHANGED
})
class ManyToManyProtectedField(ManyToManyField):
def contribute_to_class(self, cls, name):
# To support multiple relations to self, it's useful to have a non-None
# related name on symmetrical relations for internal reasons. The
# concept doesn't make a lot of sense externally ("you want me to
# specify *what* on my non-reversible relation?!"), so we set it up
# automatically. The funky name reduces the chance of an accidental
# clash.
if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
self.rel.related_name = "%s_rel_+" % name
super(ManyToManyField, self).contribute_to_class(cls, name)
# The intermediate m2m model is not auto created if:
# 1) There is a manually specified intermediate, or
# 2) The class owning the m2m field is abstract.
# 3) The class owning the m2m field has been swapped out.
if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped:
self.rel.through = create_many_to_many_protected_intermediary_model(self, cls)
# Add the descriptor for the m2m relation
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
# Set up the accessor for the m2m table name for the relation
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
# Populate some necessary rel arguments so that cross-app relations
# work correctly.
if isinstance(self.rel.through, six.string_types):
def resolve_through_model(field, model, cls):
field.rel.through = model
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)

Django: Adding objects to a related set without saving to DB

I'm trying to write an internal API in my application without necessarily coupling it with the database.
class Product(models.Model):
name=models.CharField(max_length=4000)
price=models.IntegerField(default=-1)
currency=models.CharField(max_length=3, default='INR')
class Image(models.Model):
# NOTE -- Have changed the table name to products_images
width=models.IntegerField(default=-1)
height=models.IntegerField(default=-1)
url=models.URLField(max_length=1000, verify_exists=False)
product=models.ForeignKey(Product)
def create_product:
p=Product()
i=Image(height=100, widght=100, url='http://something/something')
p.image_set.add(i)
return p
Now, when I call create_product() Django throws up an error:
IntegrityError: products_images.product_id may not be NULL
However, if I call p.save() & i.save() before calling p.image_set.add(i) it works. Is there any way that I can add objects to a related object set without saving both to the DB first?
def create_product():
product_obj = Product.objects.create(name='Foobar')
image_obj = Image.objects.create(height=100, widght=100, url='http://something/something', product=product_obj)
return product_obj
Explanation:
Product object has to be created first and then assign it to the Image object because id and name here is required field.
I am wondering why wouldn't you not require to make a product entry in DB in first case? If there is any specific reason then i may suggest you some work around?
EDIT: Okay! i think i got you, you don't want to assign a product to an image object initially. How about creating a product field as null is equal to true.
product = models.ForeignKey(Product, null=True)
Now, your function becomes something like this:
def create_product():
image_obj = Image.objects.create(height=100, widght=100, url='http://something/something')
return image_obj
Hope it helps you?
I got same issue with #Saurabh Nanda
I am using Django 1.4.2. When I read in django, i see that
# file django/db/models/fields/related.py
def get_query_set(self):
try:
return self.instance._prefetched_objects_cache[rel_field.related_query_name()]
except (AttributeError, KeyError):
db = self._db or router.db_for_read(self.model, instance=self.instance)
return super(RelatedManager,self).get_query_set().using(db).filter(**self.core_filters)
# file django/db/models/query.py
qs = getattr(obj, attname).all()
qs._result_cache = vals
# We don't want the individual qs doing prefetch_related now, since we
# have merged this into the current work.
qs._prefetch_done = True
obj._prefetched_objects_cache[cache_name] = qs
That 's make sese, we only need to set property _prefetched_objects_cache for the object.
p = Product()
image_cached = []
for i in xrange(100):
image=Image(height=100, widght=100, url='http://something/something')
image_cached.append(image)
qs = p.images.all()
qs._result_cache = image_cached
qs._prefetch_done = True
p._prefetched_objects_cache = {'images': qs}
Your problem is that the id isn't set by django, but by the database (it's represented in the database by an auto-incremented field), so until it's saved there's no id. More about this in the documentation.
I can think of three possible solutions:
Set a different field of your Image model as the primary key (documented here).
Set a different field of your Production model as the foreign key (documented here).
Use django's database transactions API (documented here).