Hide critical data in template based on user access level - python

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:
Is it safe to send all the secret_keys to the template and hide them for non logged-in users there?
(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.

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.

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>

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.

Related

What will be the most efficient way to filter the objects in django

I am working on a basic ecommerce website.I have created a superuser page in which I would like to see all the orders as well as the order details like customer who ordered, their address etc. These are my models.py:
class ShippingAddress(models.Model):
customer=models.ForeignKey(Customer,on_delete=models.SET_NULL,null=True)
order=models.ForeignKey(Order,on_delete=models.SET_NULL,null=True)
address=models.CharField(max_length=200,null=False)
email=models.EmailField(null=False)
class Customer(models.Model):
user=models.OneToOneField(MyUser,null=True,blank=True,on_delete=models.CASCADE)
email=models.CharField(max_length=100)
class Order(models.Model):
customer=models.ForeignKey(Customer,on_delete=models.SET_NULL,null=True,blank=True)
complete=models.BooleanField(default=False,null=True,blank=False)
class OrderItem(models.Model):
product=models.ForeignKey(Product,on_delete=models.SET_NULL,null=True)
order=models.ForeignKey(Order,on_delete=models.SET_NULL,null=True,)
And this is my views.py:
def superuser(request):
user=User.objects.all()
customer=Customer.objects.all()
order=Order.objects.filter(complete=True)
items=OrderItem.objects.all()
shipping=ShippingAddress.objects.all()
return render(request,"superuser.html",{'order':order,'items':items,'customer':customer,'shipping':shipping})
Currently in my template I am unable to iterate over the above context such that I can get all the orders and with the every order I can print their orderitems as well as shipping details as well as customer details. I tried in one way which was really in efficient that was to iterate over all orders then iterate over all orderitems and check if orderitem.order.id was equal to order.id . Please tell me what is the best method to pass the objects in context which are the most efficient for my need . And how to iterate over them in my template.
Thanks
How about this?
# view
from django.shortcuts import render
from .models import Order
def superuser(request):
orders = Order.objects.select_related('customer__user')
orders = orders.prefetch_related('shippingaddress_set')
orders = orders.prefetch_related('orderitem_set')
return render(request,"superuser.html",{'orders':orders})
You can of course chain the .select_related and .prefetch_related calls on the same lines, I've split them up here for increased readability. You can read about select_related and prefetch_related in the docs. You can now use the 'orders' QuerySet in a template like this:
<!--template-->
{% if orders %}
<ul>
{% for order in orders %}
<li>order id: {{order.id}}</li>
<li>customer name: {{order.customer.id}}</li>
<li>customer email: {{order.customer.email}}</li>
{% if order.shippingaddress_set.all %}
<li>Shipping addresses:
<ul>
{% for shipadd in order.shippingaddress_set.all %}
<li>shipping address: {{shipadd.address}}</li>
{% endfor %}
</ul>
</li>
{% endif %}
{% if order.orderitem_set.all %}
<li>Order items:
<ul>
{% for item in order.orderitem_set.all %}
<li>orderitem id: {{item.id}}</li>
{% endfor %}
</ul>
</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
Using Django Debug Toolbar and going to the page above, I'm informed that 3 SQL queries are made:
One fetches data from the 'order' table, doing a LEFT OUTER JOIN with the 'customers' table ON customer id.
One fetches data from the 'shippingaddress' table, with a WHERE clause specifying rows that have a 'order_id' IN (<list of all order id's>).
A similar query is made for the 'orderitem' table.
This way, Django preemptively fetches all the required data from the database, rather than e. g. doing another query for every iteration.
You might find it strange that I included a loop for the shipping addresses in the HTML template. This is however necessary, because the way you've set up your models, there is a many-to-one relationship from shipping addresses to orders. This doesn't make a lot of sense IMO, so you will probably want to redefine the ShippingAddress model's relationships to the other models.
If you want to use additional information related to each order, you can of course add more prefetch_related/select_related calls before putting the QuerySet in your context.

How to display the children of a wagtail page

My requirement sounds simple, but I am struggling to make it work
In my wagtail project I have a BlogListingPage which has many child pages. I am trying to create a list of the children in a web page. Here is my code:
models.py
class BlogListingPage(Page):
...
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
context['blog_pages'] = BlogDetailPage.objects.live().child_of(self)
return context
class BlogDetailPage(Page):
...
blog_listing = models.ForeignKey(BlogListingPage,
on_delete=models.PROTECT,
blank=True,
null=True,)
views.py
def blog(request):
url = 'blog/blog_listing_page.html'
context = {'data': BlogListingPage.objects.all()}
return render(request, url, context)
blog_listing_page.html
{% block content %}
{% for blog_listing in data %}
{% for post in blog_listing.blogdetailpage %}
{{ post.blog_title }}
{% endfor %}
{% endfor %}
{% endblock content %}
I am lost in the fog of the django/wagtail pages/objects/querysets/all()/specific
Can someone please lead me to clear blue water?
A couple of suggestions regarding your approach:
Having a foreign key from BlogDetailPage to BlogListingPage is not the Wagtail way to organise parent-child relationships. All Page objects already have a tree hierarchy and you should rely on that, otherwise you lose all the built-in tree handling provided by Wagtail.
You should use the parent/subpage rules to specify how page types can be related to each other. In you case it would look something like:
class BlogListingPage(Page):
subpage_types = ['myapp.BlogDetailPage']
class BlogDetailPage(Page):
parent_page_types = ['myapp.BlogListingPage']
The get_context() method on BlogListingPage only runs when Wagtail is serving that specific page in a view. It doesn't run when you're iterating over these pages in your blog view. In order to access the children there, you need to do something like this:
{% for blog_listing in data %}
{% for post in blog_listing.get_children.live %}
{{ post.blog_title }}
{% endfor %}
{% endfor %}
i.e., you need to use the get_children() method on the page to obtain it's children. live() further filters this to exclude unpublished pages. Note that this will only work if the blog posts are children (in the Wagtail page tree sense) of the listing page. specific() isn't important here if you're only using the post URL and title - it is relevant if you want to display fields that are part of your model rather than the base Page model.

Django: Using backwards relationships in views.py

I'm trying to pass an object to my HTML template consisting of the parent object and all child objects that relate to it. For instance:
Model Chamber:
class Chamber(models.Model):
chamber_name = models.CharField(max_length=100)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
Has ChamberProperty:
class ChamberProperty(models.Model):
chamber = models.ForeignKey(Chamber, on_delete=models.CASCADE)
property_name = models.CharField(max_length=50)
property_value = models.CharField(max_length=100)
user_defined = models.BooleanField(default=True)
They are two separate models because the customer can add as many properties as they want to their chamber.
In my views.py
class ChambersView(generic.DetailView):
template_name = 'pages/chambers.html'
def get(self, request):
user = User.objects.get(username=request.user)
customer = Customer.objects.get(user=user)
chambers_list = list(Chamber.objects.filter(customer=customer))
try:
chamber_properties = list(ChamberProperty.objects.filter(chamber__in=chambers_list).order_by('id'))
except:
chamber_properties = "No properties"
form = ChambersFilterForm(request=request)
return render(request, self.template_name, {'filter_form':form, 'chambers_list': chambers_list, 'chamber_properties': chamber_properties})
Now, that does get me a list of all chambers, and a list of all chamber properties. Except they're not linked to each other. I'm not sure how to build a list of related objects. I read about backwards relationships just now, but I don't seem to grasp how to use them.
I tried the following:
chambers_and_props = Chamber.chamberproperty_set.all()
And I get the following error:
AttributeError: 'ReverseManyToOneDescriptor' object has no attribute 'all'
So I'm not quite sure how to use it. The threads I saw mentioned that a relationship in Django automatically add a reverse on ForeignKeys and the usage should be parent.child_set.all() with child in lower caps.
I get a ReverserManyToOneDescriptor object, but not sure how to turn that into a usable list I can pass on to my HTML template.
Any ideas as to what I'm doing wrong?
Your query does not work because you have not specified which Chamber you want to get the backwards relation for.
However, this is not the right approach. Presumably you want the ChamberProperty so you can list them against each Chamber in the template. So, you should follow the relationship there in the template - there's no need to query ChamberProperty separately in the view at all.
{% for chamber in chamber_list %}
{{ chamber.chamber_name }}
{% for property in chamber.chamberproperty_set.all %}
{{ property.property_name }} : {{ property.property_value }}
{% endfor %}
{% endfor %}
You are getting the error because you are trying Chamber.chamberproperty_set on the model Chamber. It will work on individual chamber instances:
You can do this in the view:
for chamber in chambers_list
properties = chamber.chamberproperty_set.all()
Or in the template:
{% for chamber in chambers_list %}
{{ chamber }}
{% for property in chamber.chamberproperty_set.all %}
{{ property }}
{% endfor %}
Then, in your view, you can use prefetch_related to reduce the number of SQL queries:
chambers_list = Chamber.objects.filter(customer=customer).prefetch_related('chamberproperty_set')

Django templates - Conditionally displaying a button for logged in users

I have a page in my Django app that needs to do one of the following depending on the status of the logged in user in relation to a group (not a Django user group; something custom to my app) represented on the page:
If the user can join the group, display a link to join the group.
If the user is in the group, display a link to leave the group.
If the user can't join the group, don't display either link.
One way of doing this would be to create three templates (one with the join link, one with the leave link, and one with no link) and choose the appropriate one in the view. I feel like it may be overkill to have three different templates that will only differ in one line of code, so I have not gone that route as of yet.
Displaying the correct content for conditions 1 and 2 exclusively using a template is not possible, and if it were I do not think it would be advisable. Users to groups is a many-to-many relationship, and determining group membership requires passing a user to the group or passing a group to the user.
Since Django templates don't allow passing function arguments, I am trying to solve this by passing a context variable to the template using get_context_data.
def get_context_data(self, **kwargs):
context = super(NetworkDetails, self).get_context_data(**kwargs)
user = ???
context['in_group'] = user.in_group(context['group_detail'])
return context
If I do that, how can I get the currently logged in user within that method? If that isn't possible, where else can I get that information outside of the template? Is there an accepted method of doing something like this?
Thanks!
One way of doing this would be to create three templates (one with the
join link, one with the leave link, and one with no link) and choose
the appropriate one in the view
That's funny because if you can choose what template to include, you can just choose what html to display. Instead of:
{% if join_link %}
{% include 'join_link.html' %}
{% endif %}
{% if leave_link %}
{% include 'leave_link.html' %}
{% endif %}
{% if not join_link and not leave_link %}
you can't join
{% endif %}
You could just have:
{% if join_link %}
join
{% endif %}
{% if leave_link %}
leave
{% endif %}
{% if not join_link and not leave_link %}
you can't join
{% endif %}
So, I don't understand why you want to use template inclusion.
If I do that, how can I get the currently logged in user within that method?
self.request.user
self.request.user.is_authenticated() # return True if the user is logged in
You can determine the condition in your view and pass appropriate flag(s) to the template using context.
If there are multiple views/templates that need this info, you could implement custom context processor which can add this info in context and its available in each template.
Or If you have any OneToOne or any such relationship with User in your app, you can implement method in that model.
You can check if a user is logged in by checking permissions
https://docs.djangoproject.com/en/dev/topics/auth/ also you can do the similar for your other needs.

Querying Many to many fields in django template

This may not be relevant but just wanted to ask,
IF an object is passed from views to template and in the template will i be able to query many to many fields
Models code:
class Info(models.Model):
xls_answer = models.TextField(null=True,blank=True)
class Upload(models.Model):
access = models.IntegerField()
info = models.ManyToManyField(Info)
time = models.CharField(max_length=8, null=True,blank=True)
error_flag = models.IntegerField()
def __unicode__(self):
return self.access
Views:
// obj_Arr contains all the objects of upload
for objs in obj_Arr:
logging.debug(objs.access)
logging.debug(objs.time)
return render_to_response('upload/new_index.html', {'obj_arr': obj_Arr , 'load_flag' : 2})
In template is it possible to decode the many to many field since we are passing the object
Thanks..
In general, you can follow anything that's an attribute or a method call with no arguments through pathing in the django template system.
For the view code above, something like
{% for objs in obj_arr %}
{% for answer in objs.answers.all %}
{{ answer.someattribute }}
{% endfor %}
{% endfor %}
should do what you're expecting.
(I couldn't quite make out the specifics from your code sample, but hopefully this will illuminate what you can get into through the templates)
It's also possible to register a filter like this:
models.py
class Profile(models.Model):
options=models.ManyToManyField('Option', editable=False)
extra_tags.py
#register.filter
def does_profile_have_option(profile, option_id):
"""Returns non zero value if a profile has the option.
Usage::
{% if user.profile|does_profile_have_option:option.id %}
...
{% endif %}
"""
return profile.options.filter(id=option_id).count()
More info on filters can be found here https://docs.djangoproject.com/en/dev/howto/custom-template-tags/

Categories

Resources