Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
A tested example of GFK fields¶
This tutorial project
uses the lino_book.projects.gfktest
demo application
to illustrate some aspects of GenericForeignKey fields.
The models.py
file defines four database models:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from lino.api import dd
from lino.core.gfks import GenericForeignKey
class Member(dd.Model):
name = models.CharField(max_length=200)
email = models.EmailField(max_length=200, blank=True)
def __str__(self):
return self.name
class Comment(dd.Model):
allow_cascaded_delete = ['owner']
owner_type = dd.ForeignKey(ContentType)
owner_id = models.PositiveIntegerField()
owner = GenericForeignKey('owner_type', 'owner_id')
text = models.CharField(max_length=200)
def __str__(self):
return '%s object' % (self.__class__.__name__)
class Note(dd.Model):
owner_type = dd.ForeignKey(ContentType)
owner_id = models.PositiveIntegerField()
owner = GenericForeignKey('owner_type', 'owner_id')
text = models.CharField(max_length=200)
def __str__(self):
return '%s object' % (self.__class__.__name__)
class Memo(dd.Model):
owner_type = dd.ForeignKey(ContentType, blank=True, null=True)
owner_id = models.PositiveIntegerField(blank=True, null=True)
owner = GenericForeignKey('owner_type', 'owner_id')
text = models.CharField(max_length=200)
def __str__(self):
return '%s object' % (self.__class__.__name__)
A Member is the potential owner of the other three things.
A Comment has allow_cascaded_delete and thus will be silently deleted if the owner gets deleted. A Note does not allow cascaded delete, and thus will cause a veto when we try to delete a member which is owner of some note. A Memo has a nullable owner field and thus will be cleared when we delete the owner.
This project also uses lino.modlib.contenttypes
. We define this
in our settings.py
module:
from lino.projects.std.settings import *
class Site(Site):
# demo_fixtures = ['demo']
catch_layout_exceptions = False
def get_installed_plugins(self):
yield super(Site, self).get_installed_plugins()
yield 'lino.modlib.gfks'
yield 'lino_book.projects.gfktest.lib.gfktest'
A utility function:
>>> def status():
... return [m.objects.all().count() for m in [Member, Comment, Note, Memo]]
...
We create a member and three GFK-related objects whose owner fields point to that member. And then we try to delete that member.
>>> mbr = Member(name="John")
>>> mbr.save()
>>> Comment(owner=mbr, text="Just a comment").save()
>>> Note(owner=mbr, text="John owes us 100€").save()
>>> Memo(owner=mbr, text="About John and his friends").save()
>>> print(status())
[1, 1, 1, 1]
The disable_delete
method also sees these objects:
>>> print(mbr.disable_delete())
Cannot delete member John because 1 notes refer to it.
This means that Lino would prevent users from deleting this member through the web interface.
Lino also protects normal application code from deleting a member:
>>> mbr.delete()
Traceback (most recent call last):
...
Warning: Cannot delete member John because 1 notes refer to it.
All objects are still there:
>>> print(status())
[1, 1, 1, 1]
The above behaviour is thanks to a pre_delete_handler which Lino adds automatically.
We can disable this pre_delete_handler and use Django’s raw delete method in order produce broken GFKs:
>>> from django.db.models.signals import pre_delete
>>> from lino.core.model import pre_delete_handler
>>> pre_delete.disconnect(pre_delete_handler) in (None, True)
True
(Above syntax is because Django 1.6 returns None while 1.7+ returns True)
Now deleting the member will not fail:
>>> from django.db import models
>>> models.Model.delete(mbr) in (None, (1, {u'gfktest.Member': 1}))
True
Note: With Django 1.8 , the method models.Model.delete() doesn’t return anything, while since 1.8 it returns a dict describing the number of objects deleted.
And it will leave the GFK-related objects in the database.
>>> print(status())
[0, 1, 1, 1]
The users of a Lino application can see these broken GFKs by opening
the BrokenGFKs
table:
>>> rt.show(gfks.BrokenGFKs)
...
================= ======================== ======================================================== ========
Database model Database object Message Action
----------------- ------------------------ -------------------------------------------------------- --------
`comment <…>`__ `Comment object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` delete
`note <…>`__ `Note object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` manual
`memo <…>`__ `Memo object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` clear
================= ======================== ======================================================== ========
TODO: a django-admin command to cleanup broken GFK fields. This would execute the suggested actions (delete and clear) without any further user interaction. Attention:
Note that in plain Django you can achieve some of the above things by using GenericRelation fields. That is, if we define a GenericRelation from Member to every model which potentially points to it. In our case three GenericRelation objects.
A detailed comparison is yet to be written, but it seems that Django’s approach is uncomplete compared to what Lino can do.
Tested twice¶
This tutorial project is tested twice. Most things which we tested in the present document are also being tested in a plain unittest module:
# -*- coding: UTF-8 -*-
# Copyright 2015-2021 Rumma & Ko Ltd
# License: GNU Affero General Public License v3 (see file COPYING for details)
# go gfktest
# python manage.py test
from django.db import models
from django.conf import settings
from lino.api import rt
from lino.utils.djangotest import TestCase
class TestCase(TestCase):
maxDiff = None
def test01(self):
"""We create a member, and three GFK-related objects whose `owner`
fields point to that member. And then we try to delete that
member.
"""
Member = rt.models.gfktest.Member
Note = rt.models.gfktest.Note
Memo = rt.models.gfktest.Memo
Comment = rt.models.gfktest.Comment
BrokenGFKs = rt.models.gfks.BrokenGFKs
def check_status(*args):
for i, m in enumerate((Member, Comment, Note, Memo)):
n = m.objects.all().count()
if n != args[i]:
msg = "Expected %d objects in %s but found %d"
msg %= (args[i], m.__name__, n)
self.fail(msg)
gfklist = [(f.model, f.fk_field, f.ct_field)
for f in settings.SITE.kernel.GFK_LIST]
self.assertEqual(gfklist, [(Comment, 'owner_id', 'owner_type'),
(Memo, 'owner_id', 'owner_type'),
(Note, 'owner_id', 'owner_type')])
def create_objects():
mbr = Member(name="John", id=1)
mbr.save()
self.assertEqual(mbr.name, "John")
Comment(owner=mbr, text="Just a comment...").save()
Note(owner=mbr, text="John owes us 100€").save()
Memo(owner=mbr, text="More about John and his friends").save()
return mbr
mbr = create_objects()
check_status(1, 1, 1, 1)
try:
mbr.delete()
except Warning as e:
self.assertEqual(
str(e),
"Cannot delete member John because 1 notes refer to it.")
else:
self.fail("Expected an exception")
# they are all still there:
check_status(1, 1, 1, 1)
# delete the note manually
Note.objects.all().delete()
check_status(1, 1, 0, 1)
mbr.delete()
# the memo remains:
check_status(0, 0, 0, 1)
Memo.objects.all().delete()
# The above behaviour is thanks to a `pre_delete_handler`
# which Lino adds automatically. Theoretically it is no longer
# possible to produce broken GFKs. But now we disable this
# `pre_delete_handler` and use Django's raw `delete` method in
# order to produce some broken GFKs:
from django.db.models.signals import pre_delete
from lino.core.model import pre_delete_handler
pre_delete.disconnect(pre_delete_handler)
check_status(0, 0, 0, 0)
mbr = create_objects()
check_status(1, 1, 1, 1)
models.Model.delete(mbr)
pre_delete.connect(pre_delete_handler)
# The member has been deleted, but all generic related objects
# are still there:
check_status(0, 1, 1, 1)
# That's what the BrokenGFKs table is supposed to show:
# rst = BrokenGFKs.request().table2rst()
rst = BrokenGFKs.request().to_rst()
# print(rst)
self.assertEqual(
rst, """\
================= ======================== ======================================================== ========
Database model Database object Message Action
----------------- ------------------------ -------------------------------------------------------- --------
`comment <…>`__ `Comment object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` delete
`note <…>`__ `Note object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` manual
`memo <…>`__ `Memo object <…>`__ Invalid primary key 1 for gfktest.Member in `owner_id` clear
================= ======================== ======================================================== ========
""")