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?

1

4 Answers 4

14

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')
Sign up to request clarification or add additional context in comments.

13 Comments

Will calling instance.save trigger another post_save signal that will call the function again?
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?
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.
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.
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.
|
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)

2 Comments

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.
AttributeError: 'module' object has no attribute 'commit_on_success'
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.

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.