Can I limit the visible Collections in the Wagtail Admin Image Chooser? - django

I have a few Groups and Collections setup to take advantage of the Collections feature in wagtail.
I have limited collection A to Administrators only.
After logging in as a non-Administrator and clicking on the 'CHOOSE AN IMAGE' button to bring up the image chooser, there's a drop down for 'Collection' and it includes all of my collections, including the restricted collection A.
Is it possible to only show collections and images that the user owns similar to how the 'Images' menu item works?
Wagtail: 1.12.2
Django: 1.8.18

I know it has been a while since this question was asked but hopefully this answer helps some people.
Solution Overview
Wagtail 1.10 (as at this post, 2.10 is in development) introduced a helpful hook called construct_image_chooser_queryset. This hook will intercept the images before being returned to the image chooser modal (used across all of Wagtail admin) and lets a developer customise what images are returned.
The hook also has access to the request, from there you can work out the user and build up your image query accordingly.
The other part of your question involved the collections drop-down field, this requires a bit of Django template work but is doable without too much extra complexity. Any Wagtail admin template can be overridden with a custom template, simply name it using the existing template path.
The Django shared template (include) that is used to render the collection dropdown, as at posting is wagtail/admin/templates/wagtailadmin/shared/collection_chooser.html
Example Code
Note: the code is not a full solution as the question did not have the collection model changes but will be a good start hopefully.
1. Wagtail image chooser queryset hook
Based on the example in the hooks documentation
file: wagtail_hooks.py
from wagtail.core import hooks
#hooks.register('construct_image_chooser_queryset')
def show_my_uploaded_images_only(images, request):
# Example: Only show uploaded images based on current user
# actual filtering will need to be based on the collection's linked user group
images = images.filter(uploaded_by_user=request.user)
return images
2. collection chooser template override
2a file: ../templates/wagtailadmin/shared/collection_chooser.html
We want to override the template and redirect it to a custom one ONLY IF images are present in the template's context
{% extends images|yesno:"base/include/images_collection_chooser.html,wagtailadmin/shared/collection_chooser.html" %}
2b file: ../templates/base/include/images_collection_chooser.html
This template will only be used if there are images in the context
Here we revise the context to remove images and filter collections then render the original Wagtail admin template
{% load filter_with_permissions %}
{% with images=None %}
{% with collections=collections|filter_with_permissions:user %}
Images Only Collection Template:
{% include "wagtailadmin/shared/collection_chooser.html" %}
{% endwith %}
{% endwith %}
2c file: bakerydemo/base/templatetags/filter_with_permissions.py
Here we make the Django template tag that does the collection filtering
from django import template
register = template.Library()
#register.filter(name='filter_with_permissions')
def filter_with_permissions(collections, user):
""" filter the collections based on current user """
return collections.filter(name__startswith='B') # actual filter to be done

Related

How are Django's form assets (Media class) served?

The "Form Assets" page of Django says
Django allows you to associate different files – like stylesheets and scripts – with the forms and widgets that require those assets. For example, if you want to use a calendar to render DateFields, you can define a custom Calendar widget. This widget can then be associated with the CSS and JavaScript that is required to render the calendar. When the Calendar widget is used on a form, Django is able to identify the CSS and JavaScript files that are required, and provide the list of file names in a form suitable for inclusion on your web page.
Okay, but what component of Django takes those form assets and actually puts them in the page?
I ask because I'm trying to use django-autocomplete-light (DAL) with django-bootstrap5 on a multi-select form with a many-to-many field. I've done all the installing, put the widget on my form field:
class SomeForm(forms.ModelForm):
class Meta:
model = Something
fields = ['things']
widgets = {
'things': autocomplete.ModelSelect2Multiple(
url='myapp:things-autocomplete')
}
I know the widget url is correct because if it's wrong, Django barks. I know the widget is being put onto the field because it changes (from listing all things to a blank square).
I can see the HTML for the form widget includes autocomplete-light stuff to multi-select "things" with "things" autocomplete:
...
<select name="things" class="form-select" required id="id_things"
data-autocomplete-light-language="en"
data-autocomplete-light-url="/things-autocomplete/"
data-autocomplete-light-function="select2" multiple>
</select>
...
However, I don't see anywhere that select2.js is included in this web page.
I used django-autocomplete-light's ModelSelect2Multiple, which has the Select2WidgetMixin that defines a media property with all kinds of good looking stuff:
return forms.Media(
js=(
'admin/js/vendor/select2/select2.full.js',
'autocomplete_light/autocomplete_light%s.js' % extra,
'autocomplete_light/select2%s.js' % extra,
) + i18n_file,
css={
'screen': (
'admin/css/vendor/select2/select2%s.css' % extra,
'admin/css/autocomplete.css',
'autocomplete_light/select2.css',
),
},
)
I do not see any of that good-looking stuff in my web page.
I'm running with debug=True (dev mode) for now, so I believe that static files should be served as long as I have the django.contrib.staticfiles app in my settings, which I do.
Which Django component should be serving up DAL's media, and how?
I'm using Django 4.1, django-autocomplete-light 3.9.4, django-bootstrap5 22.2.
P.S. I saw other questions that seemed related, but not exactly this situation.
EDIT: It looks like form assets are loaded in a template with {{form.media}}. Maybe I have to put that media in the right place (the HTML head)?
I got this to work by adding a script block, and adding jquery and the form media manually:
{% block head_script %}
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>
{{form.media}}
{% endblock head_script %}
Not sure if there's a more automatic way to include widget media.

Django: is it safe to include secret data in the context dict used to render a template in a view?

Is data that is included in the context dict passed to the render function but is not called in the template accessible to the user or their browser?
Is it ok to just pass a QuerySet to the template even when some of the fields of the models must be kept secret from the user?
I would appreciate a reference to the official Django documentation or any other reliable sources confirming this is safe, if anyone finds one.
Code Example
models.py:
class Riddle(models.Model):
question = models.TextField() # show to user
solution = models.TextField() # very secret
views.py
def riddles_list(request):
data = Riddle.objects.all()
return render(request, 'riddles.html', {'riddles': data})
riddles.html
<ul>
{% for riddle in riddles %}
<li>{{ riddle.question }}</li>
{% endfor %}
</ul>
Is this safe, or does the user has a way of accessing the solution of the riddle? Is the solution now available anywhere except the Django server?
A possible approach would be to change the query to:
data = Riddle.objects.values('question')
Is this considered better code than just fetching the whole object? (for security, efficiency, readability, maintainability etc.) In the real code there are more calls like filter and annotate in the query, and the models have more fields.
Yes, of course, it is ok as long as you don't render that value to the template.
because the context is only accessible on the backend rendering engine for HTML pages.
whenever the page is rendered and sent to the browser, no one can access it.
Is data that is included in the context dict passed to the render function but is not called in the template accessible to the user or their browser?
The data won't be accessible to the user in their browser as long as you don't render it. Once you render the page, what you see is the resulting string from django rendering the template.
You can read more about it here, specifically this paragraph under Templates.
Django defines a standard API for loading and rendering templates regardless of the backend. Loading consists of finding the template for a given identifier and preprocessing it, usually compiling it to an in-memory representation. Rendering means interpolating the template with context data and returning the resulting string.

Django - Rendering tooltips stored in the database in templates

I'm building a Django site and in various places I want to display a little question mark and if the user hovers over it, it should display a message. I think the best approach is to store the tooltip in the database like this:
class Tooltip(models.Model):
key = models.Charfield(max_length=20)
message = models.Charfield(max_length=300)
My question is what is the best way to get the tooltips in the templates? So far my idea is to create a custom template tag, which will do the lookup and return the result, but I haven't used template tags much and wondering if there is a problem with this approach?
{% tooltip 'message_key' %}
The Django documentation explains how to create template tags.
Could be as simple as:
from django import template
register = template.Library()
#register.simple_tag
def tooltip(key):
try:
tooltip = Tooltip.objects.get(key=key)
return tooltip.message
except Tooltip.DoesNotExist:
return '' # or return a message stating that the key does not exist.
You need to save this file in your app's templatetags/ directory, along with an __init__.py. You can then {% load "<appname>" %} in your template and use the tag like you suggested.
While you might not need a database to store these, if you do, at least add an index to the key field on your model.

Django: rendering more than one view in a template

I am newbie in Django and didn't know how to render more than one view in a template. This is my case, I have a template (main.html) and I have 2 sections: a home page and one at the top where the user data (messages, log off, etc ...) will be displayed.
My question is this, can I make 2 partial views (_index.html and _userdata.html) and render them separately and display them in the template. In the same way that the "include" php or ASP.NET MVC partial views. Or spend the model or models to the template with all the information.
As I haven't explained well, an example of real life would be, Amazon or any online book store. Where you can see the books or products on the right side and have your shopping cart with your products shown. How would that Django? Have an HTML template and view the 2 models you send or you can do 2 separate views and are rendered in the same HTML template?
Those things are bound to the user, so you could get them in your parent templates.
Example:
parent.html
{{ request.user.get_something }}
define get_something(self) in the user model (your items in the cart, etc).
You can also do something like:
{% include 'elements/my_something.html' %}

Add extra data to choices in Django Admin

I have something like the following:
class Destination(models.Model):
name = models.CharField
picture = models.ImageField
def __unicode__(self):
return u"%s" % self.name
class Vacation(models.Model):
destination = models.ForeignKey(Destination)
When creating the model in my Django Admin interface, I'd like my Destinations to be displayed as radio buttons with the Destination name and Destination picture.
I'm using a custom add_form template so displaying radio buttons with destination name is no problem, but including the picture is difficult.
I would like to leave __unicode__(self) as-is, I only need the picture returned with the object in this admin view. Also I don't want to inline the object.
Any advice on how to do this (including how to incorporate it into the template) would be great!
EDIT: This SO post comes very close to what I need, but I would like to access the individual choice data instead of parsing it from a modified label.
This is not an admin-specific answer, but I think it should work in admin if you can use a custom template for the form.
You could make a modified widget (probably as a subclass of an existing Django widget), that send extra fields from the model to a custom widget template.
You could also render the form manually in the template where it's displayed, and make an inclusion tag that fetches any extra information using the id of your destination object, which is passed as the value of the option.
For example:
your_template.html
{% load destinations %}
{% for opt in form.destination %}
{{ opt.tag }}
{% destination opt.data.value %}
{% endfor %}
destinations.py (in your_app/templatetags)
from django import template
from your_app.models import Destination
register = template.Library()
#register.inclusion_tag('your_app/destination-option.html')
def destination(id):
destination=Destination.objects.filter(id=int(id)).first()
return {'destination':destination}
destination-option.html
<!-- any formatting you prefer -->
{{destination.title}}
<img src="{{destination.picture.image_url}}">