Search code examples
pythondjangodjango-modelsdjango-querysetdjango-managers

DJANGO - Queryset and model design


So I've been stuck with a design problem for the last couple of days and have sunk countless hours into it, to no avail.

My problem is that I wish return all the active Articles. I have made a method within the model, however am unable to use .filter(is_active=True)which would be the worlds best solution.

So now I have made the method into one long filter in the ArticleManager, the problem being that I cannot seem to figure out a way to count the current clicks in a way that can be useful to me. (The current_clicks method in the Article model is what I am aiming for).

Models.py

class ArticleManager(models.Manager):
    def get_queryset(self):
            return super(ArticleManager, self).get_queryset().filter(article_finish_date=None).filter(article_publish_date__lte=timezone.now()) 
#this is where i need something to the effect of .filter(article_max_clicks__gt=click_set.count())

    class Article(models.Model):
        article_name_text = models.CharField(max_length=200)
        article_max_clicks = models.IntegerField(default=0)
        article_creation_date = models.DateTimeField('date created')
        article_publish_date = models.DateTimeField('date published', null=True, blank=True)
        article_finish_date = models.DateTimeField('date finished', null=True, blank=True)
        def __str__(self):
            return self.article_name_text
        def is_active(self):
            if self.article_finish_date==None:
                if self.article_publish_date <= timezone.now():
                    return self.current_clicks() < self.article_max_clicks
                else:
                    return False
            else:
                return False
        def current_clicks(self):
            return self.click_set.count()
        is_active.boolean = True
        actives = ArticleManager()

class Click(models.Model):
    click_article = models.ForeignKey(Article, on_delete=models.CASCADE) 
    click_user = models.ForeignKey(User, on_delete=models.CASCADE)
    click_date = models.DateTimeField('date clicked')
    def __str__(self):
        return str(self.id) + " " + str(self.click_date)

This is how the clicks are created in views.py if this helps

article.click_set.create(click_article=article, click_user=user, click_date=timezone.now())

If anyone has any sort of idea of how abouts I should do this it would be greatly appreciated!

Many thanks in advance, just let me know if you need anymore information!


Solution

  • Django's annotate functionality is great for adding properties at the time of querying. From the docs -

    Per-object summaries can be generated using the annotate() clause. When an annotate() clause is specified, each object in the QuerySet will be annotated with the specified values.

    In order to keep your querying performance-minded, you can use this in your manager and not make the (possibly very slow) call of related objects for each of your Articles. Once you have an annotated property, you can use it in your query. Since Django only executes your query when objects are called, you can use this annotation instead of counting the click_set, which would call a separate query per related item. The current_clicks method may still be useful to you, but if calling it for multiple articles your queries will add up quickly and cause a big performance hit.

    Please note - I added a related_name of clicks keyword arg to your click_article field in order to use it in place of 'click_set'.

    In addition, you'll see the use of Q objects in the query below. What this allows us to do is chain together multiple filters together. These can be nested while using AND (,) / OR(|) operands. So, a reading of the Q objects below would be:

    Find all articles where article publish date is before now AND (article has no finish date OR article finish date is after now)

    from django.db.models.query import Q,Count
    
    class ArticleManager(models.Manager):
        def get_queryset(self):
            return super(ArticleManager, self).get_queryset().filter(
                        Q(article_publish_date__lte=timezone.now()),
                        ( Q(article_finish_date__isnull=True)|
                          Q(article_finish_date__gte=timezone.now())
                    ).annotate(
                        click_count=Count('clicks')
                    ).filter(
                        article_max_clicks__gt=click_count
                    )
    
    class Article(models.Model):
        actives = ArticleManager()
        def current_clicks(self):
            return self.clicks.count()
    
    # Now you can call Article.actives.all() to get all active articles        
    
    class Click(models.Model):
        click_article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='clicks') # added a related_name for more explicit calling of prefetch_related