16

I'm new to django, and as a learning app, I'm building an expense logging application.

In my models I have three classes that look like this (I simplified them slightly for brevity):

class AbstractExpense(models.Model):
    description = models.CharField(max_length=100)
    amount      = models.IntegerField()
    category    = models.ForeignKey('Category')
    tags        = models.ManyToManyField('Tag')
    insert_date = models.DateTimeField(auto_now=True)

    class Meta(object):
        abstract = True

class Expense(AbstractExpense):
    date        = models.DateField('Date')

class RecurringExpense(AbstractExpense):
    FREQUENCY_CHOICES = (('D', 'daily'),
                         ('W', 'weekly'),
                         ('M', 'monthly'),
                         ('Y', 'yearly'))
    start_date = models.DateField(blank=False)
    end_date = models.DateField(blank=True, null=True)
    last_check = models.DateField(blank=True, null=True)
    frequency = models.CharField(blank=False, max_length=1, choices=FREQUENCY_CHOICES)

RecurringExpense is just a template: when the system realises that the time to insert a recurring expense (e.g.: the rent) it should take the info in the template and copy them in a new instance of the Expense class. Here's the relevant bit from the RecurringExpense method in charge of the work:

Expense(description=self.description,
        amount=self.amount,
        category=self.category,
        # tags=self.tags,
        date=expense_date).save()

The above works flawlessly, but if I uncomment the tags=self.tags line, django complains and throw the following error:

Exception Type: TypeError
Exception Value: 'tags' is an invalid keyword argument for this function
Exception Location: <snip>/django/db/models/base.py in __init__, line 367

I know I could create a loop to work around this problem, but I wonder if there is a more elegant way that would allow me to perform the same at once....

Koedlt
  • 4,286
  • 8
  • 15
  • 33
mac
  • 42,153
  • 26
  • 121
  • 131

2 Answers2

21

You cannot set an m2m field directly like that when creating a model instance. Try the following instead:

expense = Expense(description=self.description,
        amount=self.amount,
        category=self.category,
        date=expense_date)
expense.save()
expense.tags.add(*self.tags.all())

You can check https://docs.djangoproject.com/en/1.4/topics/db/examples/many_to_many/ for more examples on how to work with many-to-many relationships.

Botond Béres
  • 16,057
  • 2
  • 37
  • 50
  • Hi and thank you for your answer. Is there a specific reason why you use the `.add()` method in combination with the unpacking of the list, rather than a simple assignment (see my own answer)? – mac Apr 10 '12 at 17:33
  • I often don't need to replace the existing m2m relation set, just add to it, so more used to that. For your use case indeed assignment is simpler and should work fine, as `ManyRelatedObjectsDescriptor` does `.clear` + `.add(*values)` behind the scenes when you do an assignment. – Botond Béres Apr 10 '12 at 19:00
  • What's the * doing here? I see that it works with it, and doesn't work without it, but I'm struggling to see documentation on why... – awidgery Jun 10 '15 at 13:39
  • 1
    @awidgery This is a basic functionality of Python, see https://docs.python.org/2/tutorial/controlflow.html#unpacking-argument-lists "unpacks" a list into function arguments. – Botond Béres Jun 29 '15 at 12:59
15

The simpliest method I could come up with:

e = Expense(description=self.description,
            amount=self.amount,
            category=self.category,
            date=expense_date)
e.save()
e.tags = self.tags.all()
mac
  • 42,153
  • 26
  • 121
  • 131
  • 1
    You can also replace `Expense(...)`/`e.save()` with `Expense.objects.create(...)` – Tomasz Zieliński Dec 17 '13 at 12:59
  • This might not work if you have a large number of tags (SQL driver dependent). In this case you can iterate in large chunks over all tags in order to add them. – odedfos Mar 17 '14 at 09:10