Kim Stacks
Kim Stacks

Reputation: 10812

How to display recent items for personalization in a web app?

In GitHub, you will notice that when you want to reference existing issues or mark an issue under projects GitHub intelligently will show you the most recent items that you visited recently first.

Example of showing recent projects in order of last visited

enter image description here

Example of showing recent issues in order of last visited enter image description here

I’m using django to build a web app and i like to incorporate the same feature.

I have two ideas but none implemented yet:

1. One giant table storing all the visits by individual users

Should I have one giant table that stores all the items that all users visit? If so how do I then join that back with the main table I am supposed to search on to produce the sort correctly?

2. Add a visited column to the individual main tables

I cannot obviously add a new date time column called “visited” to the main table as that will mean there is no personalization.

What am I missing in terms of understanding?

Also, is it a fair assumption that I need to limit the number of most recent items stored per user?

Meaning to say, using the GitHub example, only store a maximum of 5 most recent projects or issues per user.

Upvotes: 3

Views: 725

Answers (3)

Daniel Backman
Daniel Backman

Reputation: 5241

I think using Visit as a separate model is a good choice to keep separation.

It's fine to just limit the return result such as User.recent_visits.all()[:5] in the DRF api.

The solution below ensures User.recent_visits.all() will return 5 last recent visits without slicing, and avoid a huge list.

The "pattern" I've used successfully in the past which would roughly translate to this context as:

class VisitQuerySet(models.QuerySet):
    MAX_LRV = 5 # Max Last Recent Visits

    def add_item(self, user, item):
        with transaction.atomic():
            # to avoid duplicate items
            lrv, created = self.get_or_create(user=user, item=item)

            if not created:
                lrv.save()  # update timestamp
                return lrv

            self.prune_lrv(user)
        return lrv

    def prune_lrv(self, user):
        # One example of how to clean up the recent visits
        qs = self.filter(user=user)
        if qs.count() > self.MAX_LRV:
            pending_delete = qs[self.MAX_LRV:]
            self.filter(user=user, pk__in=pending_delete).delete()


class Visit(models.Model):
    class Meta:
        ordering = ('-last_visited',) # ordering important

    user = ForeignKey(User, related_name="recent_visits")
    item = ForeignKey(Item, related_name="visitors")
    last_visited = DateTimeField(auto_now=True)

    objects = VisitQuerySet.as_manager()


 # Adding a item
 Visit.objects.add_item(user, item)

 #Get last 5 recent visits
 request.user.recent_visits.all()

The list is maintained when adding new items, it's easy to change the MAX_LRV and should be straightforward to avoid duplicates (depending on how item is created in this scenario).

Upvotes: 0

Saad Aleem
Saad Aleem

Reputation: 1745

1. One giant table storing all the visits by individual users

Here's how I would go about implementing this:

class Visit(models.Model):
    user = ForeignKey(User, related_name="recent_visits")
    item = ForeignKey(Item, related_name="visitors")
    last_visited = DateTimeField()

Then, in the User serializer you could have a SerializerMethodField to match a certain amount of recently visited items, like this:

class VisitSerializer(models.Model):
    item = YourItemSerializer()

    class Meta:
        model = Visit
        fields = ['item', 'last_visited']


class UserSerializer(serializers.ModelSerializer):
    recent_visits = serializers.SerializerMethodField()

    def get_items(self, obj):
        # you could also get the value of 5 from a constant or your settings or 
        # even the API itself
        items = Item.recent_visits.filter(user=obj)[:5]
        serializer = VisitSerializer(instance=items, many=True)
        return serializer.data

This could, however, be an overkill if you only want to keep a small list of recently visited items and don't want to have any logic based on the user's extended visit history.

Keeping the item information in the user model

If you only want to show certain info about the item (like id, name etc), you could keep that information in a JSONField within the user model and only visit the item when the user wants its details. You could save database queries this way but will have to wrangle some JSON when the user visits or revisits an item.

This approach would also depend on how often the fields of Item that you want to save in the User model are changed or if the item gets deleted altogether.

Upvotes: 0

Scott Evans
Scott Evans

Reputation: 373

Would having a table that contains a FK to user, the url and the datetime last visited not work? Then create an API (drf) to return the most recent 5 for the user:

visits.objects.filter(user=request.user).order_by('-datetime_visited')[:5]

It would grow big, perhaps you can limit to only keeping 5 per user and delete as you insert. In every get request for each page you could:

last = visits.objects.filter(user=request.user).order_by('datetime_visited').first()
last.delete()
visits.objects.create(user=request.user, url=page_url, datetime_visited=datetime.datetime.now())

Probably a good idea to put a custom clean in there too: Limit the number of records in a Model that can have a value per user

Upvotes: 1

Related Questions