How to get the value of rendered field in django admin

I create pytest test and want to get rendered field which logic can change depending from user. It can be tricky due to a singleton problem in the admin How to solve the singleton problem in Django ModelAdmin. - DEV Community. The code is like:

from django.test import Client
from models import Message

@pytest.fixture(scope="session")
def manager1_browser_client(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():
        manager = User.objects.get(username='manager1')
        client = Client()
        client.manager_user = manager
        client.login(username=manager.username, password=settings.ADMIN_PASSWORD)
        yield client

def test_manager_cannot_see_sensitive_data(manager1_browser_client):
    ...
    msg = Message.objects.create()
    url = reverse(f"admin:{app_name}_{message_model_name}_change", args=[msg.pk])
    response = manager1_browser_client.get(url)

    # How to assert, that restricted data is not in field of msg.data in the admin pannel, but can be in another rendered fields of this object? 

I get from manager1_browser_client.get(url) the response object. And this not works response.context.keys() with error

{TypeError}TypeError(“unhashable type: ‘ContextDict’”)

Why does it happen? I would like to get all possible keys, because I can’t get my field value by response.context[‘data’] that was discussed here How to access context from a custom django-admin command? .

I want to get the value that was returned in the reality in one of fileds for test. And I don’t want to call ModelAdmin object directly or use beautifullsoup for that. Can I do it via .context? Or are any other good ways?

Did you print the response object? What you get? print(type(response.context))

Print here also this:
context_dict = dict(response.context) print(context_dict.keys())

<class ‘django.test.utils.ContextList’>

{TypeError}TypeError(“unhashable type: ‘ContextDict’”)

Ok, let’s try this :
flat_context = response.context.flatten()
print(flat_context.keys())

I suspect the flattening is messed up somehow when you directly call response.context.keys()

{AttributeError}AttributeError(“‘ContextList’ object has no attribute ‘flatten’”)

safe_keys = set()

for ctx in response.context:
    
    for d in ctx.dicts: 
        for key in d.keys():
            if isinstance(key, str): 
                safe_keys.add(key)

print(sorted(safe_keys))
          for key in d.keys():

E AttributeError: ‘RequestContext’ object has no attribute ‘keys’

safe_keys = set()

for ctx in response.context:

   flat_dict = ctx.flatten()
   safe_keys.update(flat_dict.keys())

print(sorted(safe_keys))
        for ctx in response.context:
>           flat_dict = ctx.flatten()

... 

    def flatten(self):
        """
        Return self.dicts as one dictionary.
        """
        flat = {}
        for d in self.dicts:
>           flat.update(d)
E           ValueError: dictionary update sequence element #0 has length 1; 2 is required


d          = [{'True': True, 'False': False, 'None': None}, {}, {}, {'site_title': 'Django Admin', 'site_header': '...rib.admin.templatetags.base.InclusionAdminNode object at 0x730ee7e76fe0>, <TextNode: '\n\n</div>\n</form></div>\n'>]>}]
flat       = {'False': False, 'None': None, 'True': True}
self       = [{'True': True, 'False': False, 'None': None}, [{'True': True, 'False': False, 'None': None}, {}, {}, {'site_title': '...ib.admin.templatetags.base.InclusionAdminNode object at 0x730ee7e76fe0>, <TextNode: '\n\n</div>\n</form></div>\n'>]>}]]

/usr/local/lib/python3.10/site-packages/django/template/context.py:120: ValueError

safe_keys = set()
for ctx in response.context:
    for d in getattr(ctx, 'dicts', []):
        if isinstance(d, dict):
            safe_keys.update([k for k in d.keys() if isinstance(k, str)])
       
        elif isinstance(d, list):
            for item in d:
                if isinstance(item, dict):
                    safe_keys.update([k for k in item.keys() if isinstance(k, str)])

print(sorted(safe_keys))

The output is:

[
  "DEFAULT_MESSAGE_LEVELS",
  "False",
  "LANGUAGE_BIDI",
  "LANGUAGE_CODE",
  "None",
  "True",
  "absolute_url",
  "add",
  "add_related_url",
  "adminform",
  "app_label",
  "app_list",
  "attrs",
  "available_apps",
  "block",
  "can_add_related",
  "can_change_related",
  "can_delete_related",
  "can_view_related",
  "change",
  "change_related_template_url",
  "content_type_id",
  "csrf_token",
  "date_label",
  "docsroot",
  "error_class",
  "errors",
  "field",
  "fieldset",
  "forloop",
  "form_url",
  "group_choices",
  "group_index",
  "group_name",
  "has_absolute_url",
  "has_add_permission",
  "has_change_permission",
  "has_delete_permission",
  "has_editable_inline_admin_formsets",
  "has_file_field",
  "has_permission",
  "has_view_permission",
  "inline_admin_formsets",
  "is_hidden",
  "is_nav_sidebar_enabled",
  "is_popup",
  "is_popup_var",
  "label",
  "log_entries",
  "media",
  "messages",
  "model",
  "model_has_limit_choices_to",
  "name",
  "object_id",
  "option",
  "opts",
  "original",
  "perms",
  "preserved_filters",
  "rendered_widget",
  "request",
  "save_as",
  "save_on_top",
  "show_changelinks",
  "site_header",
  "site_title",
  "site_url",
  "subtitle",
  "tag",
  "time_label",
  "title",
  "to_field",
  "to_field_var",
  "url_params",
  "use_tag",
  "user",
  "view_related_url_params",
  "widget"
]

That is the exact list of variables the Django Admin passes to the template to render the page. Does it make sense?

Do you see it?
So something like that might work:

def get_rendered_field_value(adminform, field_name):

for fieldset in adminform:
    for line in fieldset:
        for admin_field in line:
            # Read-only fields store the name in a dict; editable fields store it as an attribute
            current_name = admin_field.field['name'] if admin_field.is_readonly else admin_field.field.name
            
            if current_name == field_name:
                if admin_field.is_readonly:
                    return admin_field.contents() # The HTML/text rendered to the user
                return admin_field.field.value()  # The value passed to the form widget
                
return None # Field is not rendered on the page at all

Nice! It works get_rendered_field_value(response.context['adminform'], "my_field")

Thank you so much for your guidance!

So, is the iteration by adminform and fieldsets the one way to do it? I can’t just index them or get values by key as I see.

It seems that in order to get the rendered value of any field (especially read-only ones), iteration is the best way to do it. You cannot just index adminform['my_field'] directly because adminform is not a dictionary; it is a specialized generator object designed specifically to yield HTML layout blocks (Fieldsets, which yield Lines, which yield Fields).