views:

328

answers:

2

I am developing a vocabulary training program with Django (German-Swedish).

The app's vocabulary data consists of a large number of "vocabulary cards", each of which contains one or more German words or terms that correspond to one or more Swedish terms.

Training is only available for registered users, because the app keeps track of the user's performance by saving a score for each vocabulary card.

Vocabulary cards have a level (basic, advanced, expert) and any number of tags assigned to them.

When a registered user starts a training, the application needs to calculate the user's average scores for each of the levels and tags, so he can make his selection.

I have solved this problem by introducing a model named CardByUser that has a score and field and ForeignKey relationships to the models User and Card. Now I can use Django's aggregation function calculate the average scores.

The big disadvantage: this works only if there is a CardByUser instance for each and every Card instance that currently exists in the DB, even if the user has only trained 100 cards. My current solution is to create all those CardByUser instances on Card creation and when a user is registered. This is, of course, rather ineficient both in terms of data base memory and of computing time (registering a user takes quite a while).

And it seems quite inelegant, which kind of bugs me the most.

Is there a better way to do this?

Maybe it is possible to tell Django the following when calculating the average score for a Card:

  • If a CardByUser for the given Card and User exists, use its score.
  • If the CardByUser doesn't exist, use a default value --> the score 0.

Can this be done? If so, how?

Edit: Clarification Thanks S.Lott's for the first answer, but I think that my problem is a bit more complicated. My bad, I'm trying to clarify using some actual code from my models.

class Card(models.Model):
    entry_sv = models.CharField(max_length=200)
    entry_de = models.CharField(max_length=200)
    ... more fields ...

class CardByUser(models.Model):
    user = models.ForeignKey(User)
    card = models.ForeignKey(Card, related_name="user_cards")
    score = models.IntegerField(default=0)
    ... more fields ...

This means many CardByUser objects are related to a single Card.

Now in my view code, I need to create a queryset of CardByUser objects that fulfill the following criteria:

  • the related Card object's tag field contains a certain string (I now that's not optimal either, but not the focus of my question...)
  • the user is the current user

Then I can aggregate over the scores. My current code looks like this (shortened) :

user_cards = CardByUser.objects.filter(user=current_user)
                               .filter(card__tags__contains=tag.name)
avg = user_cards_agg.aggregate(Avg('score'))['score__avg']

If a CardByUser for the current user and Card does not exist, it will simply not be included in the aggregation. That's why I create all those CardByUsers with a score of 0.

So how could I get rid of those? Any ideas would be appreciated!

+1  A: 

This is what methods (and perhaps properties) are for.

class OptionalFKWithDefault( models.Model ):
    another = models.ForeignKey( AnotherModel, blank=True, null=True )
    @property
    def another_score( self ):
        if self.another is None:
            return 0
        else:
            return self.another.score
S.Lott
If it's just for the getter part, I'd use property() as a decorator.
Ionuț G. Stan
+1  A: 

This may not be entirely related to your question, but it looks like CardByUser really should be a many-to-many relationship with an extra field. (see http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships)

Maybe you could alter your model this way?

class Card(models.Model):
    entry_sv = models.CharField(max_length=200)
    entry_de = models.CharField(max_length=200)
    ... more fields ...
    users = models.ManyToManyField(User, through='CardByUser')

class CardByUser(models.Model):
    user = models.ForeignKey(User)
    card = models.ForeignKey(Card)
    score = models.IntegerField(default=0)

Then you won't have to explicitely create CardByUser objects, as this is all taken care of by Django. You should be able to simplify your aggregation query as well:

user_cards = Card.objects.filter(users=current_user)
                         .filter(tags__contains=tag.name)
...
Arnaud
Thanks! Looks promising. One question, though: if there's the new ManyToManyField on Card - does CardByUser still need the ForeignKey to user (or did you forget to remove it in your code)? If it's actually needed, I'd like to know why.
m3mitsuppe
It's actually needed whenever you ‘manually’ define a many-to-many relationship using the `through` argument. See http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships : “When you set up the intermediary model, you explicitly specify foreign keys to the models that are involved in the ManyToMany relation. This explicit declaration defines how the two models are related.”
Arnaud
Ok, right. But I'm not sure about not explicitly having to create CardByUser objects, as you write in your answer. Quite to the contrary: the docs say that such a ManyToMany-relationship is created exactly by creating instances of the intermediate model. Which is exactly what I'd like to avoid.
m3mitsuppe