Bibek Bhandari
Bibek Bhandari

Reputation: 422

How to generate feed from different models in Django?

So, I have two models called apartments and jobs. It's easy to display contents of both models separately, but what I can't figure out is how to display the mix feed of both models based on the date.

jobs = Job.objects.all().order_by('-posted_on')
apartments = Apartment.objects.all().order_by('-date')

The posted date on job is represented by 'posted_by' and the posted date on apartment is represented by 'date'. How can I combine both of these and sort them according to the date posted? I tried combining both of these models in a simpler way like:

new_feed = list(jobs) + list(apartments)

This just creates the list of both of these models, but they are not arranged based on date.

Upvotes: 1

Views: 561

Answers (3)

Bossam
Bossam

Reputation: 764

I implemented this in the following ways. I Video model and Article model that had to be curated as a feed. I made another model called Post, and then had a OneToOne key from both Video & Article.

# apps.feeds.models.py
from model_utils.models import TimeStampedModel

class Post(TimeStampedModel):
    ...
    
    @cached_property
    def target(self):
        if getattr(self, "video", None) is not None:
            return self.video
        if getattr(self, "article", None) is not None:
            return self.article
        return None


# apps/videos/models.py
class Video(models.Model):
    post = models.OneToOneField(
        "feeds.Post",
        on_delete=models.CASCADE,
    )
    ...

# apps.articles.models.py
class Article(models.Model):
    post = models.OneToOneField(
        "feeds.Post",
        on_delete=models.CASCADE,
    )
    ...

Then for the feed API, I used django-rest-framework to sort on Post queryset's created timestamp. I customized serializer's method and added queryset annotation for customization etc. This way I was able to get either Article's or Video's data as nested dictionary from the related Post instance. The advantage of this implementation is that you can optimize the queries easily with annotation, select_related, prefetch_related methods that works well on Post queryset.

# apps.feeds.serializers.py

class FeedSerializer(serializers.ModelSerializer):
    type = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ("type",)


    def to_representation(self, instance) -> OrderedDict:
        ret = super().to_representation(instance)

        if isinstance(instance.target, Video):
            ret["data"] = VideoSerializer(
                instance.target, context={"request": self.context["request"]}
            ).data
        else:
            ret["data"] = ArticleSerializer(
                instance.target, context={"request": self.context["request"]}
            ).data
        return ret

    def get_type(self, obj):
        return obj.target._meta.model_name

    @staticmethod
    def setup_eager_loading(qs):
        """
        Inspired by:
        http://ses4j.github.io/2015/11/23/optimizing-slow-django-rest-framework-performance/
        """
        qs = qs.select_related("live", "article")
        # other db optimizations...
        ...
        return qs

# apps.feeds.viewsets.py
class FeedViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = FeedSerializer
    permission_classes = (IsAuthenticatedOrReadOnly,)

    def get_queryset(self):
        qs = super().get_queryset()
        qs = self.serializer_class().setup_eager_loading(qs)
        return as
    
    ...

Upvotes: 0

pkacprzak
pkacprzak

Reputation: 5629

A good way to do that is to use adapter design pattern. The idea is that we introduce an auxiliary data structure that can be used for the purpose of sorting these model objects. This method has several benefits over trying to fit both models to have the identically named attribute used for sorting. The most important is that the change won't affect any other code in your code base.

First, you fetch your objects as you do but you don't have to fetch them sorted, you can fetch all of them in arbitrary order. You may also fetch just top 100 of them in the sorted order. Just fetch what fits your requirements here:

jobs = Job.objects.all()
apartments = Apartment.objects.all()

Then, we build an auxiliary list of tuples (attribute used for sorting, object), so:

auxiliary_list = ([(job.posted_on, job) for job in jobs] 
                + [(apartment.date, apartment) for apartment in apartments])

now, it's time to sort. We're going to sort this auxiliary list. By default, python sort() method sorts tuples in lexicographical order, which mean it will use the first element of the tuples i.e. posted_on and date attributes for ordering. Parameter reverse is set to True for sorting in decreasing order i.e. as you want them in your feed.

auxiliary_list.sort(reverse=True)

now, it's time to return only second elements of the sorted tuples:

sorted_feed = [obj for _, obj in auxiliary_list]

Just keep in mind that if you expect your feed to be huge then sorting these elements in memory is not the best way to do this, but I guess this is not your concern here.

Upvotes: 1

Lemayzeur
Lemayzeur

Reputation: 8525

I suggest two ways to achieve that.

With union() New in Django 1.11.
Uses SQL’s UNION operator to combine the results of two or more QuerySets

You need to to make sure that you have a unique name for the ordered field Like date field for job and also apartment

jobs = Job.objects.all().order_by('-posted_on')
apartments = Apartment.objects.all().order_by('-date')

new_feed = jobs.union(apartments).order_by('-date')

Note with this options, you need to have the same field name to order them.

Or
With chain(), used for treating consecutive sequences as a single sequence and use sorted() with lambda to sort them

from itertools import chain

# remove the order_by() in each queryset, use it once with sorted
jobs = Job.objects.all()
apartments = Apartment.objects.all()
result_list = sorted(chain(job, apartments),
                key=lambda instance: instance.date)

With this option, you don't really need to rename or change one of your field names, just add a property method, let's choose the Job Model

class Job(models.Model):
    ''' fields '''
    posted_on = models.DateField(......)

    @property
    def date(self):
         return self.posted_on

So now, both of your models have the attribute date, you can use chain()

result_list = sorted(chain(job, apartments),
                     key=lambda instance: instance.date)

Upvotes: 1

Related Questions