18

I'm new to Django, so excuse my ignorance :)

Say I have a model that has a couple of foreign key relations, and when I create an instance of the model I want it to automatically generate new instances for the foreign key objects as well. In this case I'm modelling course enrollment as a Group, and I am referencing the specific group as a foreign key on the model.

class Course(models.Model):
    student_group = models.OneToOneField(Group, related_name="course_taken")
    teacher_group = models.OneToOneField(Group, related_name="course_taught")

    def clean(self):
        if self.id:
            try:
                self.student_group
            except Group.DoesNotExist:
                self.student_group, _ = Group.objects.get_or_create(name='_course_' + self.id + '_student')

            try:
                self.teacher_group
            except Group.DoesNotExist:
                self.teacher_group, _ = Group.objects.get_or_create(name='_course_' + self.id + '_teacher')

It seems like I can hook into the clean method of the model to do this, but I'd like to be able to wrap the whole thing up in a single transaction, so that if it fails to create the Course later on, it won't create the related Group objects. Is there any way to achieve this?

Also, am I doing the wrong thing entirely here? Does Django provide a better way to do this?

wjin
  • 954
  • 1
  • 8
  • 13
  • 1
    possible duplicate of [Can Django automatically create a related one-to-one model?](http://stackoverflow.com/questions/1652550/can-django-automatically-create-a-related-one-to-one-model) – Martijn Pieters Sep 22 '13 at 08:22

4 Answers4

13

You can use the models.signals.post_save signal to handle such case:

from django.db import models

class Course(models.Model):
    student_group = models.OneToOneField(Group, related_name="course_taken")
    teacher_group = models.OneToOneField(Group, related_name="course_taught")


def create_course_groups(instance, created, raw, **kwargs):
    # Ignore fixtures and saves for existing courses.
    if not created or raw:
        return

    if not instance.student_group_id:
        group, _ = Group.objects.get_or_create(name='_course_' + self.id + '_student')
        instance.student_group = group

    if not instance.teacher_group_id:
        teacher_group, _ = Group.objects.get_or_create(name='_course_' + self.id + '_teacher')
        instance.teacher_group = teacher_group

    instance.save()

models.signals.post_save.connect(create_course_groups, sender=Course, dispatch_uid='create_course_groups')
Maciej Gol
  • 15,394
  • 4
  • 33
  • 51
  • 1
    Will calling instance.save trigger another post_save signal that will call the function again? – wjin Sep 22 '13 at 14:46
  • I think this also requires the fields to be nullable. Is there anything I can do with the pre_save signal combined with turning off transaction autocommit? – wjin Sep 22 '13 at 15:04
  • 1
    `created` keyword is set to True for objects that are already created, and since the callback is ran _after_ db save, then the object has been already created. You can move the code to `pre_save`, but you would have to use the `try: except:` clause to check for groups and handle exceptions. Since you are creating groups that are one2one with courses, i see no need for checks at all in post_save. – Maciej Gol Sep 22 '13 at 21:35
  • 1
    I ended up doing the validation in pre_save, and enabling the Transactions middleware to wrap each request in a single transaction, since I didn't want to make the fields nullable. But your answer was otherwise helpful. – wjin Sep 25 '13 at 06:12
  • I think this approach works bad when you want to ensure atomicity of saving process. You could wrap `save` in an `atomic` decorator, but it has two problems. First, it is not obvious why the `atomic` decorator is there. Second, when you have another signal handler, which for example sends a email, and if this handler raises an exception, your entire transaction will be rolled back, which might be not what you want. – Ivan Virabyan Apr 07 '14 at 08:19
  • @IvanVirabyan, if you care about atomicity you are most likely wrapping the prior save inside a transaction. It's the callee's duty to ensure atomicity, not the signal handler's. – Maciej Gol Apr 08 '14 at 08:40
  • @kroolik, but save method doesn't know if there any signal handlers, and if these handlers should be executed within a transaction. – Ivan Virabyan Apr 08 '14 at 09:21
  • @IvanVirabyan, yes, the save method does not know, but the save's callee does. The signal handler is not aware of existence of other handlers thus it cannot implement proper rollback mechanics. The callee does, on the other hand. – Maciej Gol Apr 08 '14 at 09:38
  • @kroolik what exactly do you mean when you say "callee"? – Ivan Virabyan Apr 08 '14 at 10:09
  • @IvanVirabyan, by `callee` I mean the view that creates said models, any command that import them etc. It's better to handle such errors etc on that level (views, commands) as you have way more information at hand, and most of all you've got the context. – Maciej Gol Apr 08 '14 at 10:25
  • do you suppose that view knows what signal handlers are run during `save` method? – Ivan Virabyan Apr 08 '14 at 12:19
  • @IvanVirabyan, I'm saying that signal handlers lack enough context to perform rollback, thus it should be done at view level. – Maciej Gol Apr 08 '14 at 13:29
  • @wjin, kroolik has written a long and good answer, shouldn't him get an upvote at least? – dspjm Apr 11 '14 at 12:49
7

Ultimately I settled on:

from django.db import models, transaction
class Course(models.Model):
    student_group = models.OneToOneField(Group, related_name="course_taken")

    @transaction.commit_on_success
    def save(self, *args, **kwargs):
        if not self.student_group_id:
            self.student_group, _ = Group.objects.get_or_create(name='_course_' + self.id + '_student')

        super(Course, self).save(*args, **kwargs)

Edit (2014/12/01): @Shasanoglu is correct, the above code does not really work due to id not existing yet. You have to do related object creation after you call save (so you call super.save, create the related object, update this object and call super.save again -- not ideal. That or you omit the id from the Group name and it's fine). Ultimately though, I moved the automated-related-object creation out of the model entirely. I did it all in the save method of a custom form which was much cleaner, and gave up on using this model in the admin interface (which was why I insisted on doing all of this in the model method in the first place)

wjin
  • 954
  • 1
  • 8
  • 13
  • I have a similar problem, and I am trying to solve it with forms. Can you link to your form answer? I know this was 4 years ago, but perhaps you have some answers to [my question](https://stackoverflow.com/questions/50972039/how-to-validate-and-create-related-objects-together-with-forms). – Greg Samson Jun 23 '18 at 18:20
  • 1
    `AttributeError: 'module' object has no attribute 'commit_on_success'` – User Oct 28 '18 at 03:46
1

I used wjin's solution in a similar problem in Django 1.7. I just had to make 2 changes:

  1. Had to change commit_on_success with atomic
  2. self.id didn't work because the code runs before the id is set when creating new object. I had to use something else as Group name.

Here is what I ended up doing:

from django.db import models
from django.contrib.auth.models import Group

class Audit(models.Model):

    @transaction.atomic
    def save(self, *args, **kwargs):
        if not hasattr(self,"reAssessmentTeam"):
            self.reAssessmentTeam, _ = Group.objects.get_or_create(name='_audit_{}_{}'.format(self.project.id,self.name))

        super(Audit, self).save(*args, **kwargs)

    project = models.ForeignKey(Project, related_name = 'audits')
    name = models.CharField(max_length=100)
    reAssessmentTeam = models.OneToOneField(Group)

I know that this might cause problems if the name is too long or someone somehow manages to use the same name but I will take care of those later.

shasanoglu
  • 33
  • 6
1

Check out my project for this at https://chris-lamb.co.uk/projects/django-auto-one-to-one which can automatically create child model instances when a parent class is created.

For example, given the following model definition:

from django.db import models
from django_auto_one_to_one import AutoOneToOneModel

class Parent(models.Model):
    field_a = models.IntegerField(default=1)

class Child(AutoOneToOneModel(Parent)):
    field_b = models.IntegerField(default=2)

... creating a Parent instance automatically creates a related Child instance:

>>> p = Parent.objects.create()
>>> p.child
<Child: parent=assd>
>>> p.child.field_b
2

A PerUserData helper is provided for the common case of creating instances when a User instance is created.

Chris Lamb
  • 1,668
  • 15
  • 11