skip to Main Content

Suppose I have a Item model, where Item objects can either be public (accessible to all users) or private (accessible only to authenticated users):

class Item(models.Model):
    title = models.CharField(max_length=100)
    is_public = models.BoleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    #...
    secret_key = ...

    class Meta:
        # I need to keep items in order:
        ordering = ('-created_at',)

What I need is to list all items using a generic.ListView – keeping the order – but hide the secret_key of those items with is_public=False for anonymous users.

So in the template, I hide the secret_key if the user is not authenticated, like:

{% if request.user.is_authenticated %}
    <p>{{ item.title }} - {{ item.secret_key }}</p>
{% else %}
    <p>{{ item.title }} - This item is private. Sign up to see the secret_key!</p>
{% endif %}

and the ListView is like:

class ItemListView(ListView):
    model = Item
    paginate_by = 10

I’m aware that I can send two separate querysets for non logged-in users to the template, one for public items and the other for private ones; but I’m not sure how can I keep the order ('-created_at') in this approach.

The question is:

  1. Is it safe to send all the secret_keys to the template and hide them for non logged-in users there?

  2. (if it is safe, then) Is there a more efficient way of doing this?

I tried overriding the get_queryset method of my ItemListView and move the if condition from template to there (I think this would increase the performance, right?). I handled the situation where the users is authenticated (simply return all the objects); but for non logged-in users, I thought about somehow joining two separate querysets, one holding the public items and the other holding only the title and created_at of private items; but I didn’t find to keep the order in this approach:

class ItemListView(ListView):
    model = Item
    paginate_by = 10

    def get_queryset(self):
        if self.request.user.is_authenticated:
            return Item.objects.all()
        else:
            # ???

This was only a minimal-reproducible-example; Actually in the project, I have multiple access_levels; Each user has an access_level, based on their plan (e.g. basic, normal, pro, etc.) and each Item has an access_level; And an I’m dealing with about +100K objects, fetched from different databases (postgresql – some cached on redis) so the performance really matters here. Also the system is up-and-running now; so I prefer less fundamental solutions.

Thanks for your time. Your help is greatly appreciated.

3

Answers


  1. Another option is to annotate the queryset to add an extra attribute for display_secret_key which is going to be more efficient than checking the user access level for each item in the queryset while templating.

    from django.db.models import F, Value as V
    
    class ItemListView(ListView):
        queryset = Item.objects.all()
        paginate_by = 10
    
        def get_queryset(self):
            annotations = {
                'display_secret_key': V('')
            }
            if self.request.user.access_level == 'PRO':
                annotations['display_secret_key'] = F('secret_key')
            return (
                super().get_queryset()
                .annotate(**annotations)
            )
    

    Then in your template:

    <p>{{ item.title }} - {{ item.display_secret_key }}</p>
    
    Login or Signup to reply.
  2. Is it safe to send all the secret_keys to the template and hide them for non logged-in users there?

    Your template is rendered server-side, and the client only get the rendered markup, so yes, it is totally safe. Well, unless someone in your team messes with the template code of course 😉

    (if it is safe, then) Is there a more efficient way of doing this?

    Just filter the queryset in your view – you don’t need two distinct querysets, and filtering the queryset will not change it’s ordering.

    def get_queryset(self):
        qs = super(ItemListView, self).get_queryset()  
        if not self.request.user.is_authenticated:
            qs = qs.filter(is_private=False) 
        return qs
    

    and in your template:

    {# avoids doing the same constant lookup within the loop #}
    {% with is_auth=request.user.is_authenticated %}
    {# I assume the queryset is named "objects" in the context ?#}
    {% for item in objects %} 
        <p>{{ item.title }}{% if is_auth %} - {{ item.secret_key }}{% endif %}</p>
    {% endfor %}
    {% endwith %}
    

    EDIT: bdoubleu rightly mentions in his answer that his solution makes testing easier. If you only need fields from your model (no method call), you can also use QuerySet.values() instead:

    def get_queryset(self):
        qs = super(ItemListView, self).get_queryset()  
        fields = ["title", "created_at"]
        if self.request.user.is_authenticated:
            fields.append("secret_key")
        else:
            qs = qs.filter(is_private=False) 
    
        return qs.values(*fields)
    

    This will also make your code a bit more efficient since it doesn’t have to build full model instances.

    Login or Signup to reply.
  3. You could use 2 Templates, one for the authenticated user one for the unauthenticated. (just overwrite the get_template_names() for authentication check and add something like _sectempl.html to the found name and add the appropriate copy of the template with the secret data)

    But I would say with bruno desthuilliers that if you switched off the debug mode there could be no constellation where unauthenticated users see content within

    {% with authenticated=request.user.is_authenticated %}
      {% if authenticated %}
        do secret stuff 
      {% endif %}
    {% endwith %}
    

    or

    {% if request.user.is_authenticated %}
       hide secret stuff for all the others
    {% endif %}
    

    If you got a complex user-grouping-combination outside the standard django user right management (where you could ask for user-permissions in templates) then I would write the user_status (your “plan” or accesslevel) into the user-session (while authentication) and check for this user_status in the output-function of the attribute of the object.

    Sketch:
    Use in template:

       {% for i in object_list %}
         {{ i.name}}, {{ i.print_secret }}
       {% endfor %}
    

    In the model you create a “print_secret”-method witch returns the secret according to the previous recorded user_status in the session-data.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search