[{"data":1,"prerenderedAt":3703},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin":3,"surround:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin":3674,"ancestors:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin":3682,"children:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin":3686,"related:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin":3687},{"id":4,"title":5,"body":6,"dateModified":3641,"datePublished":3641,"description":3642,"extension":3643,"faq":3644,"keyword":3653,"kind":3654,"meta":3655,"navigation":109,"path":3664,"seo":3665,"slug":3668,"stem":3669,"topics":3670,"type":3672,"__hash__":3673},"docs\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin\u002Findex.md","Add an Export to Excel Action in Django Admin",{"type":7,"value":8,"toc":3630},"minimark",[9,29,34,62,81,501,517,521,542,1730,1783,2052,2056,2059,2322,2340,2410,2414,2421,2668,2691,2852,2856,2859,3375,3409,3496,3500,3537,3541,3562,3566,3572,3578,3587,3593,3597,3626],[10,11,12,13,18,19,23,24,28],"p",{},"Sooner or later someone asks for \"the admin list, but in Excel\". The quick answer is an admin action that dumps the selected objects, and it is covered briefly in ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002F","Export a Django Queryset to Excel",". Done properly for an admin used every day, though, the export should match what people see on the changelist — the same columns with the same headings, \"Paid\" instead of ",[20,21,22],"code",{},"paid",", local times instead of UTC — work for every model without copy-pasting, export everything matching the current filters and search (not just the 100 rows on screen), and be limited to the people allowed to take data out of the system. This page builds that as a reusable mixin, adds a custom permission and a row limit, and tests it with the Django test client. It belongs to ",[14,25,27],{"href":26},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002F","Serving Excel Files from Python Web Apps",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install django openpyxl\n","bash","",[20,42,43],{"__ignoreMap":40},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," django",[44,60,61],{"class":54}," openpyxl\n",[10,63,64,65,68,69,72,73,76,77,80],{},"The examples use a small ",[20,66,67],{},"shop"," app with customers and orders. The code was tested with Django 6.1; the admin APIs it relies on — ",[20,70,71],{},"label_for_field",", ",[20,74,75],{},"lookup_field",", action permissions and ",[20,78,79],{},"get_changelist_instance"," — have been stable for many releases.",[35,82,86],{"className":83,"code":84,"language":85,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","from django.db import models\n\nclass Customer(models.Model):\n    name = models.CharField(max_length=100)\n    country = models.CharField(max_length=2)\n\n    def __str__(self):\n        return self.name\n\nclass Order(models.Model):\n    class Status(models.TextChoices):\n        OPEN = \"open\", \"Open\"\n        PAID = \"paid\", \"Paid\"\n        CANCELLED = \"cancelled\", \"Cancelled\"\n\n    number = models.CharField(\"Order no.\", max_length=20, unique=True)\n    customer = models.ForeignKey(Customer, on_delete=models.PROTECT)\n    status = models.CharField(max_length=10, choices=Status.choices, default=Status.OPEN)\n    total = models.DecimalField(max_digits=10, decimal_places=2)\n    paid = models.BooleanField(default=False)\n    created = models.DateTimeField()\n\n    class Meta:\n        permissions = [(\"export_order\", \"Can export orders to Excel\")]\n","python",[20,87,88,104,111,135,160,179,184,196,208,213,231,251,268,284,300,305,339,363,403,432,452,463,468,479],{"__ignoreMap":40},[44,89,90,94,98,101],{"class":46,"line":47},[44,91,93],{"class":92},"s-kum","from",[44,95,97],{"class":96},"skGVy"," django.db ",[44,99,100],{"class":92},"import",[44,102,103],{"class":96}," models\n",[44,105,107],{"class":46,"line":106},2,[44,108,110],{"emptyLinePlaceholder":109},true,"\n",[44,112,114,117,120,123,127,129,132],{"class":46,"line":113},3,[44,115,116],{"class":92},"class",[44,118,119],{"class":50}," Customer",[44,121,122],{"class":96},"(",[44,124,126],{"class":125},"sHg5J","models",[44,128,28],{"class":96},[44,130,131],{"class":125},"Model",[44,133,134],{"class":96},"):\n",[44,136,138,141,144,147,151,153,157],{"class":46,"line":137},4,[44,139,140],{"class":96},"    name ",[44,142,143],{"class":92},"=",[44,145,146],{"class":96}," models.CharField(",[44,148,150],{"class":149},"sa561","max_length",[44,152,143],{"class":92},[44,154,156],{"class":155},"sP0c6","100",[44,158,159],{"class":96},")\n",[44,161,163,166,168,170,172,174,177],{"class":46,"line":162},5,[44,164,165],{"class":96},"    country ",[44,167,143],{"class":92},[44,169,146],{"class":96},[44,171,150],{"class":149},[44,173,143],{"class":92},[44,175,176],{"class":155},"2",[44,178,159],{"class":96},[44,180,182],{"class":46,"line":181},6,[44,183,110],{"emptyLinePlaceholder":109},[44,185,187,190,193],{"class":46,"line":186},7,[44,188,189],{"class":92},"    def",[44,191,192],{"class":155}," __str__",[44,194,195],{"class":96},"(self):\n",[44,197,199,202,205],{"class":46,"line":198},8,[44,200,201],{"class":92},"        return",[44,203,204],{"class":155}," self",[44,206,207],{"class":96},".name\n",[44,209,211],{"class":46,"line":210},9,[44,212,110],{"emptyLinePlaceholder":109},[44,214,216,218,221,223,225,227,229],{"class":46,"line":215},10,[44,217,116],{"class":92},[44,219,220],{"class":50}," Order",[44,222,122],{"class":96},[44,224,126],{"class":125},[44,226,28],{"class":96},[44,228,131],{"class":125},[44,230,134],{"class":96},[44,232,234,237,240,242,244,246,249],{"class":46,"line":233},11,[44,235,236],{"class":92},"    class",[44,238,239],{"class":50}," Status",[44,241,122],{"class":96},[44,243,126],{"class":125},[44,245,28],{"class":96},[44,247,248],{"class":125},"TextChoices",[44,250,134],{"class":96},[44,252,254,257,260,263,265],{"class":46,"line":253},12,[44,255,256],{"class":155},"        OPEN",[44,258,259],{"class":92}," =",[44,261,262],{"class":54}," \"open\"",[44,264,72],{"class":96},[44,266,267],{"class":54},"\"Open\"\n",[44,269,271,274,276,279,281],{"class":46,"line":270},13,[44,272,273],{"class":155},"        PAID",[44,275,259],{"class":92},[44,277,278],{"class":54}," \"paid\"",[44,280,72],{"class":96},[44,282,283],{"class":54},"\"Paid\"\n",[44,285,287,290,292,295,297],{"class":46,"line":286},14,[44,288,289],{"class":155},"        CANCELLED",[44,291,259],{"class":92},[44,293,294],{"class":54}," \"cancelled\"",[44,296,72],{"class":96},[44,298,299],{"class":54},"\"Cancelled\"\n",[44,301,303],{"class":46,"line":302},15,[44,304,110],{"emptyLinePlaceholder":109},[44,306,308,311,313,315,318,320,322,324,327,329,332,334,337],{"class":46,"line":307},16,[44,309,310],{"class":96},"    number ",[44,312,143],{"class":92},[44,314,146],{"class":96},[44,316,317],{"class":54},"\"Order no.\"",[44,319,72],{"class":96},[44,321,150],{"class":149},[44,323,143],{"class":92},[44,325,326],{"class":155},"20",[44,328,72],{"class":96},[44,330,331],{"class":149},"unique",[44,333,143],{"class":92},[44,335,336],{"class":155},"True",[44,338,159],{"class":96},[44,340,342,345,347,350,353,355,358,361],{"class":46,"line":341},17,[44,343,344],{"class":96},"    customer ",[44,346,143],{"class":92},[44,348,349],{"class":96}," models.ForeignKey(Customer, ",[44,351,352],{"class":149},"on_delete",[44,354,143],{"class":92},[44,356,357],{"class":96},"models.",[44,359,360],{"class":155},"PROTECT",[44,362,159],{"class":96},[44,364,366,369,371,373,375,377,380,382,385,387,390,393,395,398,401],{"class":46,"line":365},18,[44,367,368],{"class":96},"    status ",[44,370,143],{"class":92},[44,372,146],{"class":96},[44,374,150],{"class":149},[44,376,143],{"class":92},[44,378,379],{"class":155},"10",[44,381,72],{"class":96},[44,383,384],{"class":149},"choices",[44,386,143],{"class":92},[44,388,389],{"class":96},"Status.choices, ",[44,391,392],{"class":149},"default",[44,394,143],{"class":92},[44,396,397],{"class":96},"Status.",[44,399,400],{"class":155},"OPEN",[44,402,159],{"class":96},[44,404,406,409,411,414,417,419,421,423,426,428,430],{"class":46,"line":405},19,[44,407,408],{"class":96},"    total ",[44,410,143],{"class":92},[44,412,413],{"class":96}," models.DecimalField(",[44,415,416],{"class":149},"max_digits",[44,418,143],{"class":92},[44,420,379],{"class":155},[44,422,72],{"class":96},[44,424,425],{"class":149},"decimal_places",[44,427,143],{"class":92},[44,429,176],{"class":155},[44,431,159],{"class":96},[44,433,435,438,440,443,445,447,450],{"class":46,"line":434},20,[44,436,437],{"class":96},"    paid ",[44,439,143],{"class":92},[44,441,442],{"class":96}," models.BooleanField(",[44,444,392],{"class":149},[44,446,143],{"class":92},[44,448,449],{"class":155},"False",[44,451,159],{"class":96},[44,453,455,458,460],{"class":46,"line":454},21,[44,456,457],{"class":96},"    created ",[44,459,143],{"class":92},[44,461,462],{"class":96}," models.DateTimeField()\n",[44,464,466],{"class":46,"line":465},22,[44,467,110],{"emptyLinePlaceholder":109},[44,469,471,473,476],{"class":46,"line":470},23,[44,472,236],{"class":92},[44,474,475],{"class":50}," Meta",[44,477,478],{"class":96},":\n",[44,480,482,485,487,490,493,495,498],{"class":46,"line":481},24,[44,483,484],{"class":96},"        permissions ",[44,486,143],{"class":92},[44,488,489],{"class":96}," [(",[44,491,492],{"class":54},"\"export_order\"",[44,494,72],{"class":96},[44,496,497],{"class":54},"\"Can export orders to Excel\"",[44,499,500],{"class":96},")]\n",[10,502,503,504,507,508,511,512,516],{},"The ",[20,505,506],{},"export_order"," permission in ",[20,509,510],{},"Meta.permissions"," is what the export will check, so being able to ",[513,514,515],"em",{},"view"," orders in the admin does not automatically mean being able to download all of them.",[30,518,520],{"id":519},"step-1-a-mixin-that-exports-list_display","Step 1 — A mixin that exports list_display",[10,522,523,524,527,528,530,531,534,535,538,539,541],{},"The mixin reads the columns from the admin's own ",[20,525,526],{},"list_display",", so an export always matches the changelist. Django's admin utilities do the hard work: ",[20,529,71],{}," gives the column label the admin uses (the field's ",[20,532,533],{},"verbose_name",", which the admin capitalises with ",[20,536,537],{},"capfirst"," when it renders the heading), and ",[20,540,75],{}," resolves a field name, a related lookup or an admin method to a value:",[35,543,545],{"className":83,"code":544,"language":85,"meta":40,"style":40},"import logging\nfrom datetime import datetime\nfrom io import BytesIO\n\nfrom django.contrib import admin, messages\nfrom django.contrib.admin.utils import label_for_field, lookup_field\nfrom django.http import HttpResponse\nfrom django.urls import path\nfrom django.utils import timezone\nfrom django.utils.text import capfirst\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font\nfrom openpyxl.utils import get_column_letter\n\nlog = logging.getLogger(\"admin.export\")\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ndef excel_value(field, value):\n    if field is not None and getattr(field, \"flatchoices\", None):\n        return str(dict(field.flatchoices).get(value, value))\n    if isinstance(value, bool):\n        return \"Yes\" if value else \"No\"\n    if isinstance(value, datetime):\n        return timezone.localtime(value).replace(tzinfo=None) if timezone.is_aware(value) else value\n    if value is None or isinstance(value, (int, float)) or hasattr(value, \"as_tuple\"):\n        return value\n    text = str(value)\n    return \"'\" + text if text[:1] in (\"=\", \"+\", \"-\", \"@\") else text\n\nclass ExportXlsxMixin:\n    export_fields = None          # defaults to list_display\n    export_max_rows = 50_000\n\n    def get_export_fields(self, request):\n        fields = self.export_fields or self.get_list_display(request)\n        return [f for f in fields if f != \"action_checkbox\"]\n\n    def has_export_permission(self, request):\n        return request.user.has_perm(f\"{self.opts.app_label}.export_{self.opts.model_name}\")\n\n    def build_workbook(self, request, queryset):\n        fields = self.get_export_fields(request)\n        wb = Workbook()\n        ws = wb.active\n        ws.title = str(self.opts.verbose_name_plural)[:31]\n        ws.append([capfirst(str(label_for_field(f, self.model, self))) for f in fields])\n        for obj in queryset.iterator(chunk_size=2000):\n            row = []\n            for name in fields:\n                field, _attr, value = lookup_field(name, obj, self)\n                row.append(excel_value(field, value))\n            ws.append(row)\n        for i, name in enumerate(fields, start=1):\n            ws.cell(1, i).font = Font(bold=True)\n            ws.column_dimensions[get_column_letter(i)].width = 18\n        ws.freeze_panes = \"A2\"\n        ws.auto_filter.ref = ws.dimensions\n        buf = BytesIO()\n        wb.save(buf)\n        return buf.getvalue()\n\n    def xlsx_response(self, request, queryset, scope):\n        count = queryset.count()\n        if count > self.export_max_rows:\n            self.message_user(request, f\"{count:,} rows is more than the export limit of \"\n                                       f\"{self.export_max_rows:,}. Narrow the filters first.\", messages.ERROR)\n            return None\n        log.info(\"%s exported %d %s (%s)\", request.user, count, self.opts.verbose_name_plural, scope)\n        stamp = timezone.localtime().strftime(\"%Y-%m-%d_%H%M\")\n        response = HttpResponse(self.build_workbook(request, queryset), content_type=XLSX)\n        response[\"Content-Disposition\"] = f'attachment; filename=\"{self.opts.model_name}s_{stamp}.xlsx\"'\n        return response\n\n    @admin.action(description=\"Export selected to Excel\", permissions=[\"export\"])\n    def export_xlsx(self, request, queryset):\n        return self.xlsx_response(request, queryset, \"selected\")\n",[20,546,547,554,566,578,582,594,606,618,630,642,654,666,678,690,694,709,719,723,735,771,786,801,820,829,857,900,907,920,977,982,992,1006,1017,1022,1033,1053,1085,1090,1100,1143,1148,1159,1171,1182,1193,1215,1246,1270,1281,1295,1310,1316,1322,1347,1372,1383,1394,1405,1416,1422,1430,1435,1446,1457,1474,1500,1530,1539,1574,1595,1620,1660,1668,1673,1705,1715],{"__ignoreMap":40},[44,548,549,551],{"class":46,"line":47},[44,550,100],{"class":92},[44,552,553],{"class":96}," logging\n",[44,555,556,558,561,563],{"class":46,"line":106},[44,557,93],{"class":92},[44,559,560],{"class":96}," datetime ",[44,562,100],{"class":92},[44,564,565],{"class":96}," datetime\n",[44,567,568,570,573,575],{"class":46,"line":113},[44,569,93],{"class":92},[44,571,572],{"class":96}," io ",[44,574,100],{"class":92},[44,576,577],{"class":96}," BytesIO\n",[44,579,580],{"class":46,"line":137},[44,581,110],{"emptyLinePlaceholder":109},[44,583,584,586,589,591],{"class":46,"line":162},[44,585,93],{"class":92},[44,587,588],{"class":96}," django.contrib ",[44,590,100],{"class":92},[44,592,593],{"class":96}," admin, messages\n",[44,595,596,598,601,603],{"class":46,"line":181},[44,597,93],{"class":92},[44,599,600],{"class":96}," django.contrib.admin.utils ",[44,602,100],{"class":92},[44,604,605],{"class":96}," label_for_field, lookup_field\n",[44,607,608,610,613,615],{"class":46,"line":186},[44,609,93],{"class":92},[44,611,612],{"class":96}," django.http ",[44,614,100],{"class":92},[44,616,617],{"class":96}," HttpResponse\n",[44,619,620,622,625,627],{"class":46,"line":198},[44,621,93],{"class":92},[44,623,624],{"class":96}," django.urls ",[44,626,100],{"class":92},[44,628,629],{"class":96}," path\n",[44,631,632,634,637,639],{"class":46,"line":210},[44,633,93],{"class":92},[44,635,636],{"class":96}," django.utils ",[44,638,100],{"class":92},[44,640,641],{"class":96}," timezone\n",[44,643,644,646,649,651],{"class":46,"line":215},[44,645,93],{"class":92},[44,647,648],{"class":96}," django.utils.text ",[44,650,100],{"class":92},[44,652,653],{"class":96}," capfirst\n",[44,655,656,658,661,663],{"class":46,"line":233},[44,657,93],{"class":92},[44,659,660],{"class":96}," openpyxl ",[44,662,100],{"class":92},[44,664,665],{"class":96}," Workbook\n",[44,667,668,670,673,675],{"class":46,"line":253},[44,669,93],{"class":92},[44,671,672],{"class":96}," openpyxl.styles ",[44,674,100],{"class":92},[44,676,677],{"class":96}," Font\n",[44,679,680,682,685,687],{"class":46,"line":270},[44,681,93],{"class":92},[44,683,684],{"class":96}," openpyxl.utils ",[44,686,100],{"class":92},[44,688,689],{"class":96}," get_column_letter\n",[44,691,692],{"class":46,"line":286},[44,693,110],{"emptyLinePlaceholder":109},[44,695,696,699,701,704,707],{"class":46,"line":302},[44,697,698],{"class":96},"log ",[44,700,143],{"class":92},[44,702,703],{"class":96}," logging.getLogger(",[44,705,706],{"class":54},"\"admin.export\"",[44,708,159],{"class":96},[44,710,711,714,716],{"class":46,"line":307},[44,712,713],{"class":155},"XLSX",[44,715,259],{"class":92},[44,717,718],{"class":54}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[44,720,721],{"class":46,"line":341},[44,722,110],{"emptyLinePlaceholder":109},[44,724,725,728,732],{"class":46,"line":365},[44,726,727],{"class":92},"def",[44,729,731],{"class":730},"s_Opv"," excel_value",[44,733,734],{"class":96},"(field, value):\n",[44,736,737,740,743,746,749,752,755,758,761,764,766,769],{"class":46,"line":405},[44,738,739],{"class":92},"    if",[44,741,742],{"class":96}," field ",[44,744,745],{"class":92},"is",[44,747,748],{"class":92}," not",[44,750,751],{"class":155}," None",[44,753,754],{"class":92}," and",[44,756,757],{"class":155}," getattr",[44,759,760],{"class":96},"(field, ",[44,762,763],{"class":54},"\"flatchoices\"",[44,765,72],{"class":96},[44,767,768],{"class":155},"None",[44,770,134],{"class":96},[44,772,773,775,778,780,783],{"class":46,"line":434},[44,774,201],{"class":92},[44,776,777],{"class":155}," str",[44,779,122],{"class":96},[44,781,782],{"class":155},"dict",[44,784,785],{"class":96},"(field.flatchoices).get(value, value))\n",[44,787,788,790,793,796,799],{"class":46,"line":454},[44,789,739],{"class":92},[44,791,792],{"class":155}," isinstance",[44,794,795],{"class":96},"(value, ",[44,797,798],{"class":155},"bool",[44,800,134],{"class":96},[44,802,803,805,808,811,814,817],{"class":46,"line":465},[44,804,201],{"class":92},[44,806,807],{"class":54}," \"Yes\"",[44,809,810],{"class":92}," if",[44,812,813],{"class":96}," value ",[44,815,816],{"class":92},"else",[44,818,819],{"class":54}," \"No\"\n",[44,821,822,824,826],{"class":46,"line":470},[44,823,739],{"class":92},[44,825,792],{"class":155},[44,827,828],{"class":96},"(value, datetime):\n",[44,830,831,833,836,839,841,843,846,849,852,854],{"class":46,"line":481},[44,832,201],{"class":92},[44,834,835],{"class":96}," timezone.localtime(value).replace(",[44,837,838],{"class":149},"tzinfo",[44,840,143],{"class":92},[44,842,768],{"class":155},[44,844,845],{"class":96},") ",[44,847,848],{"class":92},"if",[44,850,851],{"class":96}," timezone.is_aware(value) ",[44,853,816],{"class":92},[44,855,856],{"class":96}," value\n",[44,858,860,862,864,866,868,871,873,876,879,881,884,887,890,893,895,898],{"class":46,"line":859},25,[44,861,739],{"class":92},[44,863,813],{"class":96},[44,865,745],{"class":92},[44,867,751],{"class":155},[44,869,870],{"class":92}," or",[44,872,792],{"class":155},[44,874,875],{"class":96},"(value, (",[44,877,878],{"class":155},"int",[44,880,72],{"class":96},[44,882,883],{"class":155},"float",[44,885,886],{"class":96},")) ",[44,888,889],{"class":92},"or",[44,891,892],{"class":155}," hasattr",[44,894,795],{"class":96},[44,896,897],{"class":54},"\"as_tuple\"",[44,899,134],{"class":96},[44,901,903,905],{"class":46,"line":902},26,[44,904,201],{"class":92},[44,906,856],{"class":96},[44,908,910,913,915,917],{"class":46,"line":909},27,[44,911,912],{"class":96},"    text ",[44,914,143],{"class":92},[44,916,777],{"class":155},[44,918,919],{"class":96},"(value)\n",[44,921,923,926,929,932,935,937,940,943,946,949,952,955,957,960,962,965,967,970,972,974],{"class":46,"line":922},28,[44,924,925],{"class":92},"    return",[44,927,928],{"class":54}," \"'\"",[44,930,931],{"class":92}," +",[44,933,934],{"class":96}," text ",[44,936,848],{"class":92},[44,938,939],{"class":96}," text[:",[44,941,942],{"class":155},"1",[44,944,945],{"class":96},"] ",[44,947,948],{"class":92},"in",[44,950,951],{"class":96}," (",[44,953,954],{"class":54},"\"=\"",[44,956,72],{"class":96},[44,958,959],{"class":54},"\"+\"",[44,961,72],{"class":96},[44,963,964],{"class":54},"\"-\"",[44,966,72],{"class":96},[44,968,969],{"class":54},"\"@\"",[44,971,845],{"class":96},[44,973,816],{"class":92},[44,975,976],{"class":96}," text\n",[44,978,980],{"class":46,"line":979},29,[44,981,110],{"emptyLinePlaceholder":109},[44,983,985,987,990],{"class":46,"line":984},30,[44,986,116],{"class":92},[44,988,989],{"class":50}," ExportXlsxMixin",[44,991,478],{"class":96},[44,993,995,998,1000,1002],{"class":46,"line":994},31,[44,996,997],{"class":96},"    export_fields ",[44,999,143],{"class":92},[44,1001,751],{"class":155},[44,1003,1005],{"class":1004},"s-wDw","          # defaults to list_display\n",[44,1007,1009,1012,1014],{"class":46,"line":1008},32,[44,1010,1011],{"class":96},"    export_max_rows ",[44,1013,143],{"class":92},[44,1015,1016],{"class":155}," 50_000\n",[44,1018,1020],{"class":46,"line":1019},33,[44,1021,110],{"emptyLinePlaceholder":109},[44,1023,1025,1027,1030],{"class":46,"line":1024},34,[44,1026,189],{"class":92},[44,1028,1029],{"class":730}," get_export_fields",[44,1031,1032],{"class":96},"(self, request):\n",[44,1034,1036,1039,1041,1043,1046,1048,1050],{"class":46,"line":1035},35,[44,1037,1038],{"class":96},"        fields ",[44,1040,143],{"class":92},[44,1042,204],{"class":155},[44,1044,1045],{"class":96},".export_fields ",[44,1047,889],{"class":92},[44,1049,204],{"class":155},[44,1051,1052],{"class":96},".get_list_display(request)\n",[44,1054,1056,1058,1061,1064,1067,1069,1072,1074,1076,1079,1082],{"class":46,"line":1055},36,[44,1057,201],{"class":92},[44,1059,1060],{"class":96}," [f ",[44,1062,1063],{"class":92},"for",[44,1065,1066],{"class":96}," f ",[44,1068,948],{"class":92},[44,1070,1071],{"class":96}," fields ",[44,1073,848],{"class":92},[44,1075,1066],{"class":96},[44,1077,1078],{"class":92},"!=",[44,1080,1081],{"class":54}," \"action_checkbox\"",[44,1083,1084],{"class":96},"]\n",[44,1086,1088],{"class":46,"line":1087},37,[44,1089,110],{"emptyLinePlaceholder":109},[44,1091,1093,1095,1098],{"class":46,"line":1092},38,[44,1094,189],{"class":92},[44,1096,1097],{"class":730}," has_export_permission",[44,1099,1032],{"class":96},[44,1101,1103,1105,1108,1111,1114,1118,1121,1124,1127,1130,1132,1134,1137,1139,1141],{"class":46,"line":1102},39,[44,1104,201],{"class":92},[44,1106,1107],{"class":96}," request.user.has_perm(",[44,1109,1110],{"class":92},"f",[44,1112,1113],{"class":54},"\"",[44,1115,1117],{"class":1116},"sSjpA","{",[44,1119,1120],{"class":155},"self",[44,1122,1123],{"class":96},".opts.app_label",[44,1125,1126],{"class":1116},"}",[44,1128,1129],{"class":54},".export_",[44,1131,1117],{"class":1116},[44,1133,1120],{"class":155},[44,1135,1136],{"class":96},".opts.model_name",[44,1138,1126],{"class":1116},[44,1140,1113],{"class":54},[44,1142,159],{"class":96},[44,1144,1146],{"class":46,"line":1145},40,[44,1147,110],{"emptyLinePlaceholder":109},[44,1149,1151,1153,1156],{"class":46,"line":1150},41,[44,1152,189],{"class":92},[44,1154,1155],{"class":730}," build_workbook",[44,1157,1158],{"class":96},"(self, request, queryset):\n",[44,1160,1162,1164,1166,1168],{"class":46,"line":1161},42,[44,1163,1038],{"class":96},[44,1165,143],{"class":92},[44,1167,204],{"class":155},[44,1169,1170],{"class":96},".get_export_fields(request)\n",[44,1172,1174,1177,1179],{"class":46,"line":1173},43,[44,1175,1176],{"class":96},"        wb ",[44,1178,143],{"class":92},[44,1180,1181],{"class":96}," Workbook()\n",[44,1183,1185,1188,1190],{"class":46,"line":1184},44,[44,1186,1187],{"class":96},"        ws ",[44,1189,143],{"class":92},[44,1191,1192],{"class":96}," wb.active\n",[44,1194,1196,1199,1201,1203,1205,1207,1210,1213],{"class":46,"line":1195},45,[44,1197,1198],{"class":96},"        ws.title ",[44,1200,143],{"class":92},[44,1202,777],{"class":155},[44,1204,122],{"class":96},[44,1206,1120],{"class":155},[44,1208,1209],{"class":96},".opts.verbose_name_plural)[:",[44,1211,1212],{"class":155},"31",[44,1214,1084],{"class":96},[44,1216,1218,1221,1224,1227,1229,1232,1234,1237,1239,1241,1243],{"class":46,"line":1217},46,[44,1219,1220],{"class":96},"        ws.append([capfirst(",[44,1222,1223],{"class":155},"str",[44,1225,1226],{"class":96},"(label_for_field(f, ",[44,1228,1120],{"class":155},[44,1230,1231],{"class":96},".model, ",[44,1233,1120],{"class":155},[44,1235,1236],{"class":96},"))) ",[44,1238,1063],{"class":92},[44,1240,1066],{"class":96},[44,1242,948],{"class":92},[44,1244,1245],{"class":96}," fields])\n",[44,1247,1249,1252,1255,1257,1260,1263,1265,1268],{"class":46,"line":1248},47,[44,1250,1251],{"class":92},"        for",[44,1253,1254],{"class":96}," obj ",[44,1256,948],{"class":92},[44,1258,1259],{"class":96}," queryset.iterator(",[44,1261,1262],{"class":149},"chunk_size",[44,1264,143],{"class":92},[44,1266,1267],{"class":155},"2000",[44,1269,134],{"class":96},[44,1271,1273,1276,1278],{"class":46,"line":1272},48,[44,1274,1275],{"class":96},"            row ",[44,1277,143],{"class":92},[44,1279,1280],{"class":96}," []\n",[44,1282,1284,1287,1290,1292],{"class":46,"line":1283},49,[44,1285,1286],{"class":92},"            for",[44,1288,1289],{"class":96}," name ",[44,1291,948],{"class":92},[44,1293,1294],{"class":96}," fields:\n",[44,1296,1298,1301,1303,1306,1308],{"class":46,"line":1297},50,[44,1299,1300],{"class":96},"                field, _attr, value ",[44,1302,143],{"class":92},[44,1304,1305],{"class":96}," lookup_field(name, obj, ",[44,1307,1120],{"class":155},[44,1309,159],{"class":96},[44,1311,1313],{"class":46,"line":1312},51,[44,1314,1315],{"class":96},"                row.append(excel_value(field, value))\n",[44,1317,1319],{"class":46,"line":1318},52,[44,1320,1321],{"class":96},"            ws.append(row)\n",[44,1323,1325,1327,1330,1332,1335,1338,1341,1343,1345],{"class":46,"line":1324},53,[44,1326,1251],{"class":92},[44,1328,1329],{"class":96}," i, name ",[44,1331,948],{"class":92},[44,1333,1334],{"class":155}," enumerate",[44,1336,1337],{"class":96},"(fields, ",[44,1339,1340],{"class":149},"start",[44,1342,143],{"class":92},[44,1344,942],{"class":155},[44,1346,134],{"class":96},[44,1348,1350,1353,1355,1358,1360,1363,1366,1368,1370],{"class":46,"line":1349},54,[44,1351,1352],{"class":96},"            ws.cell(",[44,1354,942],{"class":155},[44,1356,1357],{"class":96},", i).font ",[44,1359,143],{"class":92},[44,1361,1362],{"class":96}," Font(",[44,1364,1365],{"class":149},"bold",[44,1367,143],{"class":92},[44,1369,336],{"class":155},[44,1371,159],{"class":96},[44,1373,1375,1378,1380],{"class":46,"line":1374},55,[44,1376,1377],{"class":96},"            ws.column_dimensions[get_column_letter(i)].width ",[44,1379,143],{"class":92},[44,1381,1382],{"class":155}," 18\n",[44,1384,1386,1389,1391],{"class":46,"line":1385},56,[44,1387,1388],{"class":96},"        ws.freeze_panes ",[44,1390,143],{"class":92},[44,1392,1393],{"class":54}," \"A2\"\n",[44,1395,1397,1400,1402],{"class":46,"line":1396},57,[44,1398,1399],{"class":96},"        ws.auto_filter.ref ",[44,1401,143],{"class":92},[44,1403,1404],{"class":96}," ws.dimensions\n",[44,1406,1408,1411,1413],{"class":46,"line":1407},58,[44,1409,1410],{"class":96},"        buf ",[44,1412,143],{"class":92},[44,1414,1415],{"class":96}," BytesIO()\n",[44,1417,1419],{"class":46,"line":1418},59,[44,1420,1421],{"class":96},"        wb.save(buf)\n",[44,1423,1425,1427],{"class":46,"line":1424},60,[44,1426,201],{"class":92},[44,1428,1429],{"class":96}," buf.getvalue()\n",[44,1431,1433],{"class":46,"line":1432},61,[44,1434,110],{"emptyLinePlaceholder":109},[44,1436,1438,1440,1443],{"class":46,"line":1437},62,[44,1439,189],{"class":92},[44,1441,1442],{"class":730}," xlsx_response",[44,1444,1445],{"class":96},"(self, request, queryset, scope):\n",[44,1447,1449,1452,1454],{"class":46,"line":1448},63,[44,1450,1451],{"class":96},"        count ",[44,1453,143],{"class":92},[44,1455,1456],{"class":96}," queryset.count()\n",[44,1458,1460,1463,1466,1469,1471],{"class":46,"line":1459},64,[44,1461,1462],{"class":92},"        if",[44,1464,1465],{"class":96}," count ",[44,1467,1468],{"class":92},">",[44,1470,204],{"class":155},[44,1472,1473],{"class":96},".export_max_rows:\n",[44,1475,1477,1480,1483,1485,1487,1489,1492,1495,1497],{"class":46,"line":1476},65,[44,1478,1479],{"class":155},"            self",[44,1481,1482],{"class":96},".message_user(request, ",[44,1484,1110],{"class":92},[44,1486,1113],{"class":54},[44,1488,1117],{"class":1116},[44,1490,1491],{"class":96},"count",[44,1493,1494],{"class":92},":,",[44,1496,1126],{"class":1116},[44,1498,1499],{"class":54}," rows is more than the export limit of \"\n",[44,1501,1503,1506,1508,1510,1512,1515,1517,1519,1522,1525,1528],{"class":46,"line":1502},66,[44,1504,1505],{"class":92},"                                       f",[44,1507,1113],{"class":54},[44,1509,1117],{"class":1116},[44,1511,1120],{"class":155},[44,1513,1514],{"class":96},".export_max_rows",[44,1516,1494],{"class":92},[44,1518,1126],{"class":1116},[44,1520,1521],{"class":54},". Narrow the filters first.\"",[44,1523,1524],{"class":96},", messages.",[44,1526,1527],{"class":155},"ERROR",[44,1529,159],{"class":96},[44,1531,1533,1536],{"class":46,"line":1532},67,[44,1534,1535],{"class":92},"            return",[44,1537,1538],{"class":155}," None\n",[44,1540,1542,1545,1547,1550,1553,1556,1559,1561,1563,1566,1569,1571],{"class":46,"line":1541},68,[44,1543,1544],{"class":96},"        log.info(",[44,1546,1113],{"class":54},[44,1548,1549],{"class":1116},"%s",[44,1551,1552],{"class":54}," exported ",[44,1554,1555],{"class":1116},"%d",[44,1557,1558],{"class":1116}," %s",[44,1560,951],{"class":54},[44,1562,1549],{"class":1116},[44,1564,1565],{"class":54},")\"",[44,1567,1568],{"class":96},", request.user, count, ",[44,1570,1120],{"class":155},[44,1572,1573],{"class":96},".opts.verbose_name_plural, scope)\n",[44,1575,1577,1580,1582,1585,1588,1590,1593],{"class":46,"line":1576},69,[44,1578,1579],{"class":96},"        stamp ",[44,1581,143],{"class":92},[44,1583,1584],{"class":96}," timezone.localtime().strftime(",[44,1586,1587],{"class":54},"\"%Y-%m-",[44,1589,1555],{"class":1116},[44,1591,1592],{"class":54},"_%H%M\"",[44,1594,159],{"class":96},[44,1596,1598,1601,1603,1606,1608,1611,1614,1616,1618],{"class":46,"line":1597},70,[44,1599,1600],{"class":96},"        response ",[44,1602,143],{"class":92},[44,1604,1605],{"class":96}," HttpResponse(",[44,1607,1120],{"class":155},[44,1609,1610],{"class":96},".build_workbook(request, queryset), ",[44,1612,1613],{"class":149},"content_type",[44,1615,143],{"class":92},[44,1617,713],{"class":155},[44,1619,159],{"class":96},[44,1621,1623,1626,1629,1631,1633,1636,1639,1641,1643,1645,1647,1650,1652,1655,1657],{"class":46,"line":1622},71,[44,1624,1625],{"class":96},"        response[",[44,1627,1628],{"class":54},"\"Content-Disposition\"",[44,1630,945],{"class":96},[44,1632,143],{"class":92},[44,1634,1635],{"class":92}," f",[44,1637,1638],{"class":54},"'attachment; filename=\"",[44,1640,1117],{"class":1116},[44,1642,1120],{"class":155},[44,1644,1136],{"class":96},[44,1646,1126],{"class":1116},[44,1648,1649],{"class":54},"s_",[44,1651,1117],{"class":1116},[44,1653,1654],{"class":96},"stamp",[44,1656,1126],{"class":1116},[44,1658,1659],{"class":54},".xlsx\"'\n",[44,1661,1663,1665],{"class":46,"line":1662},72,[44,1664,201],{"class":92},[44,1666,1667],{"class":96}," response\n",[44,1669,1671],{"class":46,"line":1670},73,[44,1672,110],{"emptyLinePlaceholder":109},[44,1674,1676,1679,1681,1684,1686,1689,1691,1694,1696,1699,1702],{"class":46,"line":1675},74,[44,1677,1678],{"class":730},"    @admin.action",[44,1680,122],{"class":96},[44,1682,1683],{"class":149},"description",[44,1685,143],{"class":92},[44,1687,1688],{"class":54},"\"Export selected to Excel\"",[44,1690,72],{"class":96},[44,1692,1693],{"class":149},"permissions",[44,1695,143],{"class":92},[44,1697,1698],{"class":96},"[",[44,1700,1701],{"class":54},"\"export\"",[44,1703,1704],{"class":96},"])\n",[44,1706,1708,1710,1713],{"class":46,"line":1707},75,[44,1709,189],{"class":92},[44,1711,1712],{"class":730}," export_xlsx",[44,1714,1158],{"class":96},[44,1716,1718,1720,1722,1725,1728],{"class":46,"line":1717},76,[44,1719,201],{"class":92},[44,1721,204],{"class":155},[44,1723,1724],{"class":96},".xlsx_response(request, queryset, ",[44,1726,1727],{"class":54},"\"selected\"",[44,1729,159],{"class":96},[10,1731,1732,1734,1735,1737,1738,1741,1742,1745,1746,1749,1750,1753,1754,72,1756,72,1759,1762,1763,1766,1767,1771,1772,1775,1776,1779,1780,1782],{},[20,1733,75],{}," returns the model field (or ",[20,1736,768],{}," for an admin method), the attribute and the value, so ",[20,1739,1740],{},"excel_value"," can turn a choice code into its label using the field's ",[20,1743,1744],{},"flatchoices",", booleans into Yes\u002FNo, and aware datetimes into naive local times — Excel has no time zones, and ",[20,1747,1748],{},"openpyxl"," refuses aware datetimes. Numbers and ",[20,1751,1752],{},"Decimal"," values pass through as numbers so they can be summed in Excel. Text that starts with ",[20,1755,143],{},[20,1757,1758],{},"+",[20,1760,1761],{},"-"," or ",[20,1764,1765],{},"@"," gets an apostrophe so Excel does not treat a customer's name as a formula; the reasoning is in ",[14,1768,1770],{"href":1769},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports\u002F","Prevent Formula Injection in Excel Exports",". ",[20,1773,1774],{},"permissions=[\"export\"]"," on the action makes Django call ",[20,1777,1778],{},"has_export_permission"," and hide the action from anyone without it. Returning ",[20,1781,768],{}," from an action redirects back to the changelist, where the error message appears.",[1784,1785,1793,1794,1793,1798,1793,1802,1793,1810,1793,1819,1793,1823,1793,1830,1793,1834,1793,1838,1793,1842,1793,1846,1793,1848,1793,1852,1793,1854,1793,1857,1793,1860,1793,1863,1793,1865,1793,1869,1793,1871,1793,1874,1793,1877,1793,1881,1793,1883,1793,1886,1793,1888,1793,1891,1793,1894,1793,1897,1793,1899,1793,1902,1793,1904,1793,1906,1793,1909,1793,1913,1793,1915,1793,1917,1793,1919,1793,1921,1793,1924,1793,1928,1793,1930,1793,1933,1793,1935,1793,1938,1793,1943,1793,1948,1793,1952,1793,1956,1793,1961,1793,1966,1793,1968,1793,1970,1793,1973,1793,1977,1793,1979,1793,1981,1793,1984,1793,1988,1793,1992,1793,1996,1793,1999,1793,2002,1793,2004,1793,2007,1793,2010,1793,2014,1793,2016,1793,2019,1793,2024,1793,2028,1793,2033,1793,2038,1793,2046],"svg",{"viewBox":1786,"role":1787,"ariaLabelledBy":1788,"xmlns":1791,"style":1792},"4 0 753 248","img",[1789,1790],"dj-columns-t","dj-columns-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:753px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[1795,1796,1797],"title",{"id":1789},"From list_display to workbook columns",[1799,1800,1801],"desc",{"id":1790},"Each entry in list_display becomes a column. Its heading comes from the admin label, such as Order no. for the number field and Country for an admin method. Values are converted for Excel, so the status code paid becomes Paid, True becomes Yes and an aware UTC datetime becomes local time.",[1803,1804],"rect",{"x":1805,"y":1806,"width":1807,"height":1808,"fill":1809},"4","0","753","248","#ffffff",[1803,1811],{"x":1812,"y":1813,"width":1814,"height":1815,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[1803,1820],{"x":1821,"y":1813,"width":1822,"height":1815,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"52.0","93.9",[1824,1825,1829],"text",{"x":1826,"y":1827,"style":1828},"99.0","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[1803,1831],{"x":1832,"y":1813,"width":1833,"height":1815,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"145.9","100.2",[1824,1835,1837],{"x":1836,"y":1827,"style":1828},"196.0","B",[1803,1839],{"x":1812,"y":1840,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"64.0","28.0",[1824,1843,942],{"x":1844,"y":1845,"style":1828},"36.0","83.0",[1803,1847],{"x":1821,"y":1840,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1849,1851],{"x":1826,"y":1845,"style":1850},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","entry",[1803,1853],{"x":1832,"y":1840,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1855,1856],{"x":1836,"y":1845,"style":1850},"raw value",[1803,1858],{"x":1812,"y":1859,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"92.0",[1824,1861,176],{"x":1844,"y":1862,"style":1828},"111.0",[1803,1864],{"x":1821,"y":1859,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1866,1868],{"x":1826,"y":1862,"style":1867},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","number",[1803,1870],{"x":1832,"y":1859,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1872,1873],{"x":1836,"y":1862,"style":1867},"SO-1002",[1803,1875],{"x":1812,"y":1876,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"120.0",[1824,1878,1880],{"x":1844,"y":1879,"style":1828},"139.0","3",[1803,1882],{"x":1821,"y":1876,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1884,1885],{"x":1826,"y":1879,"style":1867},"customer_country",[1803,1887],{"x":1832,"y":1876,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1889,1890],{"x":1836,"y":1879,"style":1867},"GB",[1803,1892],{"x":1812,"y":1893,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"148.0",[1824,1895,1805],{"x":1844,"y":1896,"style":1828},"167.0",[1803,1898],{"x":1821,"y":1893,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1900,1901],{"x":1826,"y":1896,"style":1867},"status",[1803,1903],{"x":1832,"y":1893,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1905,22],{"x":1836,"y":1896,"style":1867},[1803,1907],{"x":1812,"y":1908,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"176.0",[1824,1910,1912],{"x":1844,"y":1911,"style":1828},"195.0","5",[1803,1914],{"x":1821,"y":1908,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1916,22],{"x":1826,"y":1911,"style":1867},[1803,1918],{"x":1832,"y":1908,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1920,336],{"x":1836,"y":1911,"style":1867},[1803,1922],{"x":1812,"y":1923,"width":1814,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"204.0",[1824,1925,1927],{"x":1844,"y":1926,"style":1828},"223.0","6",[1803,1929],{"x":1821,"y":1923,"width":1822,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1931,1932],{"x":1826,"y":1926,"style":1867},"created",[1803,1934],{"x":1832,"y":1923,"width":1833,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1936,1937],{"x":1836,"y":1926,"style":1867},"2026-09-03 07:15Z",[1803,1939],{"x":1940,"y":1813,"width":1941,"height":1841,"rx":1806,"fill":1816,"stroke":1817,"style":1942},"583.7","59.3","stroke-width:1.5px",[1824,1944,1947],{"x":1945,"y":1946,"style":1850},"613.3","59.0","heading",[1803,1949],{"x":1950,"y":1813,"width":1951,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},"643.0","97.0",[1824,1953,1955],{"x":1954,"y":1946,"style":1850},"691.5","cell",[1803,1957],{"x":1940,"y":1958,"width":1941,"height":1841,"rx":1806,"fill":1959,"stroke":1960,"style":1942},"68.0","#f0f4ff","var(--brand,#5b5cf0)",[1824,1962,1965],{"x":1945,"y":1963,"style":1964},"87.0","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Order no.",[1803,1967],{"x":1950,"y":1958,"width":1951,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1969,1873],{"x":1954,"y":1963,"style":1867},[1803,1971],{"x":1940,"y":1972,"width":1941,"height":1841,"rx":1806,"fill":1959,"stroke":1960,"style":1942},"96.0",[1824,1974,1976],{"x":1945,"y":1975,"style":1964},"115.0","Country",[1803,1978],{"x":1950,"y":1972,"width":1951,"height":1841,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,1980,1890],{"x":1954,"y":1975,"style":1867},[1803,1982],{"x":1940,"y":1983,"width":1941,"height":1841,"rx":1806,"fill":1959,"stroke":1960,"style":1942},"124.0",[1824,1985,1987],{"x":1945,"y":1986,"style":1964},"143.0","Status",[1803,1989],{"x":1950,"y":1983,"width":1951,"height":1841,"rx":1806,"fill":1990,"stroke":1991,"style":1942},"#d9f4f1","var(--teal,#0f9488)",[1824,1993,1995],{"x":1954,"y":1986,"style":1994},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Paid",[1803,1997],{"x":1940,"y":1998,"width":1941,"height":1841,"rx":1806,"fill":1959,"stroke":1960,"style":1942},"152.0",[1824,2000,1995],{"x":1945,"y":2001,"style":1964},"171.0",[1803,2003],{"x":1950,"y":1998,"width":1951,"height":1841,"rx":1806,"fill":1990,"stroke":1991,"style":1942},[1824,2005,2006],{"x":1954,"y":2001,"style":1994},"Yes",[1803,2008],{"x":1940,"y":2009,"width":1941,"height":1841,"rx":1806,"fill":1959,"stroke":1960,"style":1942},"180.0",[1824,2011,2013],{"x":1945,"y":2012,"style":1964},"199.0","Created",[1803,2015],{"x":1950,"y":2009,"width":1951,"height":1841,"rx":1806,"fill":1990,"stroke":1991,"style":1942},[1824,2017,2018],{"x":1954,"y":2012,"style":1994},"03\u002F09\u002F2026 08:15",[1824,2020,526],{"x":2021,"y":2022,"style":2023},"133.1","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",[1824,2025,2027],{"x":2026,"y":2022,"style":2023},"661.8","Excel column",[46,2029],{"x1":2030,"y1":1983,"x2":2031,"y2":1983,"stroke":1960,"style":2032},"256.1","569.7","stroke-width:2px",[2034,2035],"polygon",{"points":2036,"fill":2037},"573.7,124.0 564.7,129.0 564.7,119.0","#5b5cf0",[1803,2039],{"x":2040,"y":2041,"width":2042,"height":2043,"rx":2044,"fill":1809,"stroke":1817,"style":2045},"295.9","91.0","238.1","22.0","11","stroke-width:1.2px",[1824,2047,2051],{"x":2048,"y":2049,"style":2050},"414.9","106.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","label_for_field + lookup_field",[30,2053,2055],{"id":2054},"step-2-export-everything-matching-the-filters","Step 2 — Export everything matching the filters",[10,2057,2058],{},"An action only sees the rows ticked on the current page. To export everything that matches the filters and search box, add a URL to the model admin and build the queryset the same way the changelist does:",[35,2060,2062],{"className":83,"code":2061,"language":85,"meta":40,"style":40},"from django.core.exceptions import PermissionDenied\nfrom django.shortcuts import redirect\nfrom django.urls import path\n\nfrom .exports import ExportXlsxMixin\n\nclass FilteredExportMixin(ExportXlsxMixin):\n    change_list_template = \"admin\u002Fexport_change_list.html\"\n\n    def get_urls(self):\n        name = f\"{self.opts.app_label}_{self.opts.model_name}_export\"\n        return [path(\"export\u002F\", self.admin_site.admin_view(self.export_filtered_view), name=name)] + super().get_urls()\n\n    def export_filtered_view(self, request):\n        if not self.has_export_permission(request):\n            raise PermissionDenied\n        changelist = self.get_changelist_instance(request)\n        queryset = changelist.get_queryset(request)\n        return self.xlsx_response(request, queryset, \"filtered\") or redirect(\n            f\"..\u002F?{request.GET.urlencode()}\")\n",[20,2063,2064,2076,2088,2098,2102,2114,2118,2132,2142,2146,2155,2188,2226,2230,2239,2250,2257,2269,2279,2297],{"__ignoreMap":40},[44,2065,2066,2068,2071,2073],{"class":46,"line":47},[44,2067,93],{"class":92},[44,2069,2070],{"class":96}," django.core.exceptions ",[44,2072,100],{"class":92},[44,2074,2075],{"class":96}," PermissionDenied\n",[44,2077,2078,2080,2083,2085],{"class":46,"line":106},[44,2079,93],{"class":92},[44,2081,2082],{"class":96}," django.shortcuts ",[44,2084,100],{"class":92},[44,2086,2087],{"class":96}," redirect\n",[44,2089,2090,2092,2094,2096],{"class":46,"line":113},[44,2091,93],{"class":92},[44,2093,624],{"class":96},[44,2095,100],{"class":92},[44,2097,629],{"class":96},[44,2099,2100],{"class":46,"line":137},[44,2101,110],{"emptyLinePlaceholder":109},[44,2103,2104,2106,2109,2111],{"class":46,"line":162},[44,2105,93],{"class":92},[44,2107,2108],{"class":96}," .exports ",[44,2110,100],{"class":92},[44,2112,2113],{"class":96}," ExportXlsxMixin\n",[44,2115,2116],{"class":46,"line":181},[44,2117,110],{"emptyLinePlaceholder":109},[44,2119,2120,2122,2125,2127,2130],{"class":46,"line":186},[44,2121,116],{"class":92},[44,2123,2124],{"class":50}," FilteredExportMixin",[44,2126,122],{"class":96},[44,2128,2129],{"class":125},"ExportXlsxMixin",[44,2131,134],{"class":96},[44,2133,2134,2137,2139],{"class":46,"line":198},[44,2135,2136],{"class":96},"    change_list_template ",[44,2138,143],{"class":92},[44,2140,2141],{"class":54}," \"admin\u002Fexport_change_list.html\"\n",[44,2143,2144],{"class":46,"line":210},[44,2145,110],{"emptyLinePlaceholder":109},[44,2147,2148,2150,2153],{"class":46,"line":215},[44,2149,189],{"class":92},[44,2151,2152],{"class":730}," get_urls",[44,2154,195],{"class":96},[44,2156,2157,2160,2162,2164,2166,2168,2170,2172,2174,2177,2179,2181,2183,2185],{"class":46,"line":233},[44,2158,2159],{"class":96},"        name ",[44,2161,143],{"class":92},[44,2163,1635],{"class":92},[44,2165,1113],{"class":54},[44,2167,1117],{"class":1116},[44,2169,1120],{"class":155},[44,2171,1123],{"class":96},[44,2173,1126],{"class":1116},[44,2175,2176],{"class":54},"_",[44,2178,1117],{"class":1116},[44,2180,1120],{"class":155},[44,2182,1136],{"class":96},[44,2184,1126],{"class":1116},[44,2186,2187],{"class":54},"_export\"\n",[44,2189,2190,2192,2195,2198,2200,2202,2205,2207,2210,2213,2215,2218,2220,2223],{"class":46,"line":253},[44,2191,201],{"class":92},[44,2193,2194],{"class":96}," [path(",[44,2196,2197],{"class":54},"\"export\u002F\"",[44,2199,72],{"class":96},[44,2201,1120],{"class":155},[44,2203,2204],{"class":96},".admin_site.admin_view(",[44,2206,1120],{"class":155},[44,2208,2209],{"class":96},".export_filtered_view), ",[44,2211,2212],{"class":149},"name",[44,2214,143],{"class":92},[44,2216,2217],{"class":96},"name)] ",[44,2219,1758],{"class":92},[44,2221,2222],{"class":155}," super",[44,2224,2225],{"class":96},"().get_urls()\n",[44,2227,2228],{"class":46,"line":270},[44,2229,110],{"emptyLinePlaceholder":109},[44,2231,2232,2234,2237],{"class":46,"line":286},[44,2233,189],{"class":92},[44,2235,2236],{"class":730}," export_filtered_view",[44,2238,1032],{"class":96},[44,2240,2241,2243,2245,2247],{"class":46,"line":302},[44,2242,1462],{"class":92},[44,2244,748],{"class":92},[44,2246,204],{"class":155},[44,2248,2249],{"class":96},".has_export_permission(request):\n",[44,2251,2252,2255],{"class":46,"line":307},[44,2253,2254],{"class":92},"            raise",[44,2256,2075],{"class":96},[44,2258,2259,2262,2264,2266],{"class":46,"line":341},[44,2260,2261],{"class":96},"        changelist ",[44,2263,143],{"class":92},[44,2265,204],{"class":155},[44,2267,2268],{"class":96},".get_changelist_instance(request)\n",[44,2270,2271,2274,2276],{"class":46,"line":365},[44,2272,2273],{"class":96},"        queryset ",[44,2275,143],{"class":92},[44,2277,2278],{"class":96}," changelist.get_queryset(request)\n",[44,2280,2281,2283,2285,2287,2290,2292,2294],{"class":46,"line":405},[44,2282,201],{"class":92},[44,2284,204],{"class":155},[44,2286,1724],{"class":96},[44,2288,2289],{"class":54},"\"filtered\"",[44,2291,845],{"class":96},[44,2293,889],{"class":92},[44,2295,2296],{"class":96}," redirect(\n",[44,2298,2299,2302,2305,2307,2310,2313,2316,2318,2320],{"class":46,"line":434},[44,2300,2301],{"class":92},"            f",[44,2303,2304],{"class":54},"\"..\u002F?",[44,2306,1117],{"class":1116},[44,2308,2309],{"class":96},"request.",[44,2311,2312],{"class":155},"GET",[44,2314,2315],{"class":96},".urlencode()",[44,2317,1126],{"class":1116},[44,2319,1113],{"class":54},[44,2321,159],{"class":96},[10,2323,2324,2327,2328,2331,2332,2335,2336,2339],{},[20,2325,2326],{},"get_changelist_instance(request)"," builds the same ",[20,2329,2330],{},"ChangeList"," object the admin uses to render the page, from the request's query string — list filters, date hierarchy, search terms and ordering included — so ",[20,2333,2334],{},"changelist.get_queryset(request)"," returns exactly the rows the user is looking at across all pages. The view is wrapped in ",[20,2337,2338],{},"admin_site.admin_view",", which requires a logged-in staff user, and it checks the export permission itself. A small template adds a button to the changelist that carries the current query string along:",[35,2341,2345],{"className":2342,"code":2343,"language":2344,"meta":40,"style":40},"language-html shiki shiki-themes github-light github-dark-high-contrast","{% extends \"admin\u002Fchange_list.html\" %}\n{% block object-tools-items %}\n  {{ block.super }}\n  \u003Cli>\u003Ca href=\"export\u002F?{{ request.GET.urlencode }}\" class=\"historylink\">Export to Excel\u003C\u002Fa>\u003C\u002Fli>\n{% endblock %}\n","html",[20,2346,2347,2352,2357,2362,2405],{"__ignoreMap":40},[44,2348,2349],{"class":46,"line":47},[44,2350,2351],{"class":96},"{% extends \"admin\u002Fchange_list.html\" %}\n",[44,2353,2354],{"class":46,"line":106},[44,2355,2356],{"class":96},"{% block object-tools-items %}\n",[44,2358,2359],{"class":46,"line":113},[44,2360,2361],{"class":96},"  {{ block.super }}\n",[44,2363,2364,2367,2371,2374,2376,2379,2381,2384,2387,2389,2392,2395,2397,2400,2402],{"class":46,"line":137},[44,2365,2366],{"class":96},"  \u003C",[44,2368,2370],{"class":2369},"sx1sP","li",[44,2372,2373],{"class":96},">\u003C",[44,2375,14],{"class":2369},[44,2377,2378],{"class":125}," href",[44,2380,143],{"class":96},[44,2382,2383],{"class":54},"\"export\u002F?{{ request.GET.urlencode }}\"",[44,2385,2386],{"class":125}," class",[44,2388,143],{"class":96},[44,2390,2391],{"class":54},"\"historylink\"",[44,2393,2394],{"class":96},">Export to Excel\u003C\u002F",[44,2396,14],{"class":2369},[44,2398,2399],{"class":96},">\u003C\u002F",[44,2401,2370],{"class":2369},[44,2403,2404],{"class":96},">\n",[44,2406,2407],{"class":46,"line":162},[44,2408,2409],{"class":96},"{% endblock %}\n",[30,2411,2413],{"id":2412},"step-3-register-a-model","Step 3 — Register a model",[10,2415,2416,2417,2420],{},"With the mixins in place, enabling the export on a model admin is one base class and one line in ",[20,2418,2419],{},"actions",":",[35,2422,2424],{"className":83,"code":2423,"language":85,"meta":40,"style":40},"from django.contrib import admin\n\nfrom .exports_filtered import FilteredExportMixin\nfrom .models import Order\n\n@admin.register(Order)\nclass OrderAdmin(FilteredExportMixin, admin.ModelAdmin):\n    list_display = [\"number\", \"customer\", \"customer_country\", \"status\", \"total\", \"paid\", \"created\"]\n    list_filter = [\"status\", \"paid\"]\n    search_fields = [\"number\", \"customer__name\"]\n    actions = [\"export_xlsx\"]\n\n    @admin.display(description=\"Country\", ordering=\"customer__country\")\n    def customer_country(self, obj):\n        return obj.customer.country\n\n    def get_queryset(self, request):\n        return super().get_queryset(request).select_related(\"customer\")\n",[20,2425,2426,2437,2441,2453,2465,2469,2477,2501,2546,2563,2581,2595,2599,2625,2635,2642,2646,2655],{"__ignoreMap":40},[44,2427,2428,2430,2432,2434],{"class":46,"line":47},[44,2429,93],{"class":92},[44,2431,588],{"class":96},[44,2433,100],{"class":92},[44,2435,2436],{"class":96}," admin\n",[44,2438,2439],{"class":46,"line":106},[44,2440,110],{"emptyLinePlaceholder":109},[44,2442,2443,2445,2448,2450],{"class":46,"line":113},[44,2444,93],{"class":92},[44,2446,2447],{"class":96}," .exports_filtered ",[44,2449,100],{"class":92},[44,2451,2452],{"class":96}," FilteredExportMixin\n",[44,2454,2455,2457,2460,2462],{"class":46,"line":137},[44,2456,93],{"class":92},[44,2458,2459],{"class":96}," .models ",[44,2461,100],{"class":92},[44,2463,2464],{"class":96}," Order\n",[44,2466,2467],{"class":46,"line":162},[44,2468,110],{"emptyLinePlaceholder":109},[44,2470,2471,2474],{"class":46,"line":181},[44,2472,2473],{"class":730},"@admin.register",[44,2475,2476],{"class":96},"(Order)\n",[44,2478,2479,2481,2484,2486,2489,2491,2494,2496,2499],{"class":46,"line":186},[44,2480,116],{"class":92},[44,2482,2483],{"class":50}," OrderAdmin",[44,2485,122],{"class":96},[44,2487,2488],{"class":125},"FilteredExportMixin",[44,2490,72],{"class":96},[44,2492,2493],{"class":125},"admin",[44,2495,28],{"class":96},[44,2497,2498],{"class":125},"ModelAdmin",[44,2500,134],{"class":96},[44,2502,2503,2506,2508,2511,2514,2516,2519,2521,2524,2526,2529,2531,2534,2536,2539,2541,2544],{"class":46,"line":198},[44,2504,2505],{"class":96},"    list_display ",[44,2507,143],{"class":92},[44,2509,2510],{"class":96}," [",[44,2512,2513],{"class":54},"\"number\"",[44,2515,72],{"class":96},[44,2517,2518],{"class":54},"\"customer\"",[44,2520,72],{"class":96},[44,2522,2523],{"class":54},"\"customer_country\"",[44,2525,72],{"class":96},[44,2527,2528],{"class":54},"\"status\"",[44,2530,72],{"class":96},[44,2532,2533],{"class":54},"\"total\"",[44,2535,72],{"class":96},[44,2537,2538],{"class":54},"\"paid\"",[44,2540,72],{"class":96},[44,2542,2543],{"class":54},"\"created\"",[44,2545,1084],{"class":96},[44,2547,2548,2551,2553,2555,2557,2559,2561],{"class":46,"line":210},[44,2549,2550],{"class":96},"    list_filter ",[44,2552,143],{"class":92},[44,2554,2510],{"class":96},[44,2556,2528],{"class":54},[44,2558,72],{"class":96},[44,2560,2538],{"class":54},[44,2562,1084],{"class":96},[44,2564,2565,2568,2570,2572,2574,2576,2579],{"class":46,"line":215},[44,2566,2567],{"class":96},"    search_fields ",[44,2569,143],{"class":92},[44,2571,2510],{"class":96},[44,2573,2513],{"class":54},[44,2575,72],{"class":96},[44,2577,2578],{"class":54},"\"customer__name\"",[44,2580,1084],{"class":96},[44,2582,2583,2586,2588,2590,2593],{"class":46,"line":233},[44,2584,2585],{"class":96},"    actions ",[44,2587,143],{"class":92},[44,2589,2510],{"class":96},[44,2591,2592],{"class":54},"\"export_xlsx\"",[44,2594,1084],{"class":96},[44,2596,2597],{"class":46,"line":253},[44,2598,110],{"emptyLinePlaceholder":109},[44,2600,2601,2604,2606,2608,2610,2613,2615,2618,2620,2623],{"class":46,"line":270},[44,2602,2603],{"class":730},"    @admin.display",[44,2605,122],{"class":96},[44,2607,1683],{"class":149},[44,2609,143],{"class":92},[44,2611,2612],{"class":54},"\"Country\"",[44,2614,72],{"class":96},[44,2616,2617],{"class":149},"ordering",[44,2619,143],{"class":92},[44,2621,2622],{"class":54},"\"customer__country\"",[44,2624,159],{"class":96},[44,2626,2627,2629,2632],{"class":46,"line":286},[44,2628,189],{"class":92},[44,2630,2631],{"class":730}," customer_country",[44,2633,2634],{"class":96},"(self, obj):\n",[44,2636,2637,2639],{"class":46,"line":302},[44,2638,201],{"class":92},[44,2640,2641],{"class":96}," obj.customer.country\n",[44,2643,2644],{"class":46,"line":307},[44,2645,110],{"emptyLinePlaceholder":109},[44,2647,2648,2650,2653],{"class":46,"line":341},[44,2649,189],{"class":92},[44,2651,2652],{"class":730}," get_queryset",[44,2654,1032],{"class":96},[44,2656,2657,2659,2661,2664,2666],{"class":46,"line":365},[44,2658,201],{"class":92},[44,2660,2222],{"class":155},[44,2662,2663],{"class":96},"().get_queryset(request).select_related(",[44,2665,2518],{"class":54},[44,2667,159],{"class":96},[10,2669,2670,2672,2673,2675,2676,2679,2680,2683,2684,2686,2687,28],{},[20,2671,1885],{}," is an admin method with a ",[20,2674,1683],{},", which becomes both the changelist heading and the Excel heading. ",[20,2677,2678],{},"select_related(\"customer\")"," matters twice over: the changelist and the export both read the customer for every row, and without it each row costs an extra query. If an export needs columns that the changelist does not show — an internal ID, an address — set ",[20,2681,2682],{},"export_fields"," on the admin instead of changing ",[20,2685,526],{},". Making a large export run in the background instead of inside the request is covered in ",[14,2688,2690],{"href":2689},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fgenerate-large-excel-exports-in-a-background-job\u002F","Generate Large Excel Exports in a Background Job",[1784,2692,1793,2698,1793,2701,1793,2704,1793,2708,1793,2714,1793,2725,1793,2731,1793,2736,1793,2741,1793,2744,1793,2747,1793,2754,1793,2760,1793,2764,1793,2768,1793,2772,1793,2775,1793,2780,1793,2787,1793,2792,1793,2795,1793,2799,1793,2802,1793,2805,1793,2812,1793,2817,1793,2820,1793,2823,1793,2826,1793,2830,1793,2833,1793,2836,1793,2842,1793,2846,1793,2849],{"viewBox":2693,"role":1787,"ariaLabelledBy":2694,"xmlns":1791,"style":2697},"4 0 752 160",[2695,2696],"dj-request-t","dj-request-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1795,2699,2700],{"id":2695},"What happens when someone clicks Export to Excel",[1799,2702,2703],{"id":2696},"The button sends the changelist's query string to the export URL. admin_view requires a staff login, the view checks the export permission, the changelist rebuilds the filtered queryset, the row limit is checked, and the workbook is returned as an attachment.",[1803,2705],{"x":1805,"y":1806,"width":2706,"height":2707,"fill":1809},"752","160",[1803,2709],{"x":1812,"y":2710,"width":2711,"height":2712,"rx":2713,"fill":1816,"stroke":1817,"style":2032},"16.0","115.2","128.0","12",[2715,2716,2718],"g",{"transform":2717},"translate(65.6,46.5) scale(1.000)",[2719,2720],"path",{"d":2721,"fill":2722,"stroke":2723,"style":2724},"M12 3a9 9 0 1 0 .01 0z M3 12h18 M12 3c3.2 3 3.2 15 0 18 M12 3c-3.2 3-3.2 15 0 18","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[1824,2726,2730],{"x":2727,"y":2728,"style":2729},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Button",[1824,2732,2735],{"x":2727,"y":2733,"style":2734},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","export\u002F + filters",[46,2737],{"x1":2738,"y1":2739,"x2":2740,"y2":2739,"stroke":1960,"style":2032},"140.2","80.0","162.2",[2034,2742],{"points":2743,"fill":2037},"166.2,80.0 157.2,85.0 157.2,75.0",[1803,2745],{"x":2746,"y":2710,"width":2711,"height":2712,"rx":2713,"fill":1959,"stroke":1960,"style":2032},"171.2",[2715,2748,2750],{"transform":2749},"translate(216.8,37.8) scale(1.000)",[2719,2751],{"d":2752,"fill":2722,"stroke":2753,"style":2724},"M6 11h12v9H6z M8.5 11V8a3.5 3.5 0 0 1 7 0v3","var(--brand-strong,#4338ca)",[1824,2755,2759],{"x":2756,"y":2757,"style":2758},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","admin_view",[1824,2761,2763],{"x":2756,"y":2762,"style":2734},"103.8","staff login",[1824,2765,2767],{"x":2756,"y":2766,"style":2734},"121.2","required",[46,2769],{"x1":2770,"y1":2739,"x2":2771,"y2":2739,"stroke":1960,"style":2032},"291.4","313.4",[2034,2773],{"points":2774,"fill":2037},"317.4,80.0 308.4,85.0 308.4,75.0",[1803,2776],{"x":2777,"y":2710,"width":2711,"height":2712,"rx":2713,"fill":2778,"stroke":2779,"style":2032},"322.4","#fdefd8","var(--gold,#b4740a)",[2715,2781,2783],{"transform":2782},"translate(368.0,46.5) scale(1.000)",[2719,2784],{"d":2785,"fill":2722,"stroke":2786,"style":2724},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2","var(--gold-ink,#7a4e06)",[1824,2788,2791],{"x":2789,"y":2728,"style":2790},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Permission",[1824,2793,2794],{"x":2789,"y":2733,"style":2734},"shop.export_order",[46,2796],{"x1":2797,"y1":2739,"x2":2798,"y2":2739,"stroke":1960,"style":2032},"442.6","464.6",[2034,2800],{"points":2801,"fill":2037},"468.6,80.0 459.6,85.0 459.6,75.0",[1803,2803],{"x":2804,"y":2710,"width":2711,"height":2712,"rx":2713,"fill":1990,"stroke":1991,"style":2032},"473.6",[2715,2806,2808],{"transform":2807},"translate(519.2,29.0) scale(1.000)",[2719,2809],{"d":2810,"fill":2722,"stroke":2811,"style":2724},"M3 5h18l-7 8v6l-4 2v-8z","var(--teal-ink,#0b6157)",[1824,2813,2330],{"x":2814,"y":2815,"style":2816},"531.2","75.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[1824,2818,2819],{"x":2814,"y":2728,"style":2816},"queryset",[1824,2821,2822],{"x":2814,"y":2733,"style":2734},"filters, search,",[1824,2824,2617],{"x":2814,"y":2825,"style":2734},"130.0",[46,2827],{"x1":2828,"y1":2739,"x2":2829,"y2":2739,"stroke":1960,"style":2032},"593.8","615.8",[2034,2831],{"points":2832,"fill":2037},"619.8,80.0 610.8,85.0 610.8,75.0",[1803,2834],{"x":2835,"y":2710,"width":2711,"height":2712,"rx":2713,"fill":1990,"stroke":1991,"style":2032},"624.8",[2715,2837,2839],{"transform":2838},"translate(670.4,37.8) scale(1.000)",[2719,2840],{"d":2841,"fill":2722,"stroke":2811,"style":2724},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6",[1824,2843,2845],{"x":2844,"y":2757,"style":2816},"682.4","Workbook",[1824,2847,2848],{"x":2844,"y":2762,"style":2734},"attachment",[1824,2850,2851],{"x":2844,"y":2766,"style":2734},"response",[30,2853,2855],{"id":2854},"step-4-test-it-with-the-django-test-client","Step 4 — Test it with the Django test client",[10,2857,2858],{},"The tests log in as two staff users — one with the export permission, one without — and exercise both the action and the filtered export:",[35,2860,2862],{"className":83,"code":2861,"language":85,"meta":40,"style":40},"from io import BytesIO\nfrom django.contrib.auth.models import Permission, User\nfrom django.test import Client\nfrom openpyxl import load_workbook\n\nviewer = User.objects.create_user(\"viewer\", password=\"pw\", is_staff=True)\nviewer.user_permissions.add(Permission.objects.get(codename=\"view_order\"))\nexporter = User.objects.create_user(\"exporter\", password=\"pw\", is_staff=True)\nexporter.user_permissions.add(*Permission.objects.filter(codename__in=[\"view_order\", \"export_order\"]))\n\ndef rows(response):\n    ws = load_workbook(BytesIO(response.content)).active\n    return [list(r) for r in ws.iter_rows(values_only=True)]\n\nc = Client()\nc.login(username=\"viewer\", password=\"pw\")\nr = c.post(\"\u002Fadmin\u002Fshop\u002Forder\u002F\", {\"action\": \"export_xlsx\", \"_selected_action\": [\"1\", \"2\"]})\nprint(\"viewer, action:\", r.status_code, r.get(\"Content-Type\"))\nprint(\"viewer, filtered export:\", c.get(\"\u002Fadmin\u002Fshop\u002Forder\u002Fexport\u002F\").status_code)\n\nc.login(username=\"exporter\", password=\"pw\")\nr = c.post(\"\u002Fadmin\u002Fshop\u002Forder\u002F\", {\"action\": \"export_xlsx\", \"_selected_action\": [\"1\", \"2\"]})\nprint(\"exporter, action:\", r.status_code, r[\"Content-Disposition\"].split(\";\")[0])\nfor row in rows(r):\n    print(\"  \", row)\nr = c.get(\"\u002Fadmin\u002Fshop\u002Forder\u002Fexport\u002F\", {\"status__exact\": \"paid\"})\nprint(\"filtered to paid:\", [row[0] for row in rows(r)[1:]])\nprint(\"export button shown:\", \"Export to Excel\" in c.get(\"\u002Fadmin\u002Fshop\u002Forder\u002F\").content.decode())\n",[20,2863,2864,2874,2886,2898,2909,2913,2947,2963,2993,3020,3024,3034,3044,3075,3079,3089,3111,3154,3172,3190,3194,3214,3246,3273,3285,3298,3321,3351],{"__ignoreMap":40},[44,2865,2866,2868,2870,2872],{"class":46,"line":47},[44,2867,93],{"class":92},[44,2869,572],{"class":96},[44,2871,100],{"class":92},[44,2873,577],{"class":96},[44,2875,2876,2878,2881,2883],{"class":46,"line":106},[44,2877,93],{"class":92},[44,2879,2880],{"class":96}," django.contrib.auth.models ",[44,2882,100],{"class":92},[44,2884,2885],{"class":96}," Permission, User\n",[44,2887,2888,2890,2893,2895],{"class":46,"line":113},[44,2889,93],{"class":92},[44,2891,2892],{"class":96}," django.test ",[44,2894,100],{"class":92},[44,2896,2897],{"class":96}," Client\n",[44,2899,2900,2902,2904,2906],{"class":46,"line":137},[44,2901,93],{"class":92},[44,2903,660],{"class":96},[44,2905,100],{"class":92},[44,2907,2908],{"class":96}," load_workbook\n",[44,2910,2911],{"class":46,"line":162},[44,2912,110],{"emptyLinePlaceholder":109},[44,2914,2915,2918,2920,2923,2926,2928,2931,2933,2936,2938,2941,2943,2945],{"class":46,"line":181},[44,2916,2917],{"class":96},"viewer ",[44,2919,143],{"class":92},[44,2921,2922],{"class":96}," User.objects.create_user(",[44,2924,2925],{"class":54},"\"viewer\"",[44,2927,72],{"class":96},[44,2929,2930],{"class":149},"password",[44,2932,143],{"class":92},[44,2934,2935],{"class":54},"\"pw\"",[44,2937,72],{"class":96},[44,2939,2940],{"class":149},"is_staff",[44,2942,143],{"class":92},[44,2944,336],{"class":155},[44,2946,159],{"class":96},[44,2948,2949,2952,2955,2957,2960],{"class":46,"line":186},[44,2950,2951],{"class":96},"viewer.user_permissions.add(Permission.objects.get(",[44,2953,2954],{"class":149},"codename",[44,2956,143],{"class":92},[44,2958,2959],{"class":54},"\"view_order\"",[44,2961,2962],{"class":96},"))\n",[44,2964,2965,2968,2970,2972,2975,2977,2979,2981,2983,2985,2987,2989,2991],{"class":46,"line":198},[44,2966,2967],{"class":96},"exporter ",[44,2969,143],{"class":92},[44,2971,2922],{"class":96},[44,2973,2974],{"class":54},"\"exporter\"",[44,2976,72],{"class":96},[44,2978,2930],{"class":149},[44,2980,143],{"class":92},[44,2982,2935],{"class":54},[44,2984,72],{"class":96},[44,2986,2940],{"class":149},[44,2988,143],{"class":92},[44,2990,336],{"class":155},[44,2992,159],{"class":96},[44,2994,2995,2998,3001,3004,3007,3009,3011,3013,3015,3017],{"class":46,"line":210},[44,2996,2997],{"class":96},"exporter.user_permissions.add(",[44,2999,3000],{"class":92},"*",[44,3002,3003],{"class":96},"Permission.objects.filter(",[44,3005,3006],{"class":149},"codename__in",[44,3008,143],{"class":92},[44,3010,1698],{"class":96},[44,3012,2959],{"class":54},[44,3014,72],{"class":96},[44,3016,492],{"class":54},[44,3018,3019],{"class":96},"]))\n",[44,3021,3022],{"class":46,"line":215},[44,3023,110],{"emptyLinePlaceholder":109},[44,3025,3026,3028,3031],{"class":46,"line":233},[44,3027,727],{"class":92},[44,3029,3030],{"class":730}," rows",[44,3032,3033],{"class":96},"(response):\n",[44,3035,3036,3039,3041],{"class":46,"line":253},[44,3037,3038],{"class":96},"    ws ",[44,3040,143],{"class":92},[44,3042,3043],{"class":96}," load_workbook(BytesIO(response.content)).active\n",[44,3045,3046,3048,3050,3053,3056,3058,3061,3063,3066,3069,3071,3073],{"class":46,"line":270},[44,3047,925],{"class":92},[44,3049,2510],{"class":96},[44,3051,3052],{"class":155},"list",[44,3054,3055],{"class":96},"(r) ",[44,3057,1063],{"class":92},[44,3059,3060],{"class":96}," r ",[44,3062,948],{"class":92},[44,3064,3065],{"class":96}," ws.iter_rows(",[44,3067,3068],{"class":149},"values_only",[44,3070,143],{"class":92},[44,3072,336],{"class":155},[44,3074,500],{"class":96},[44,3076,3077],{"class":46,"line":286},[44,3078,110],{"emptyLinePlaceholder":109},[44,3080,3081,3084,3086],{"class":46,"line":302},[44,3082,3083],{"class":96},"c ",[44,3085,143],{"class":92},[44,3087,3088],{"class":96}," Client()\n",[44,3090,3091,3094,3097,3099,3101,3103,3105,3107,3109],{"class":46,"line":307},[44,3092,3093],{"class":96},"c.login(",[44,3095,3096],{"class":149},"username",[44,3098,143],{"class":92},[44,3100,2925],{"class":54},[44,3102,72],{"class":96},[44,3104,2930],{"class":149},[44,3106,143],{"class":92},[44,3108,2935],{"class":54},[44,3110,159],{"class":96},[44,3112,3113,3116,3118,3121,3124,3127,3130,3133,3135,3137,3140,3143,3146,3148,3151],{"class":46,"line":341},[44,3114,3115],{"class":96},"r ",[44,3117,143],{"class":92},[44,3119,3120],{"class":96}," c.post(",[44,3122,3123],{"class":54},"\"\u002Fadmin\u002Fshop\u002Forder\u002F\"",[44,3125,3126],{"class":96},", {",[44,3128,3129],{"class":54},"\"action\"",[44,3131,3132],{"class":96},": ",[44,3134,2592],{"class":54},[44,3136,72],{"class":96},[44,3138,3139],{"class":54},"\"_selected_action\"",[44,3141,3142],{"class":96},": [",[44,3144,3145],{"class":54},"\"1\"",[44,3147,72],{"class":96},[44,3149,3150],{"class":54},"\"2\"",[44,3152,3153],{"class":96},"]})\n",[44,3155,3156,3159,3161,3164,3167,3170],{"class":46,"line":365},[44,3157,3158],{"class":155},"print",[44,3160,122],{"class":96},[44,3162,3163],{"class":54},"\"viewer, action:\"",[44,3165,3166],{"class":96},", r.status_code, r.get(",[44,3168,3169],{"class":54},"\"Content-Type\"",[44,3171,2962],{"class":96},[44,3173,3174,3176,3178,3181,3184,3187],{"class":46,"line":405},[44,3175,3158],{"class":155},[44,3177,122],{"class":96},[44,3179,3180],{"class":54},"\"viewer, filtered export:\"",[44,3182,3183],{"class":96},", c.get(",[44,3185,3186],{"class":54},"\"\u002Fadmin\u002Fshop\u002Forder\u002Fexport\u002F\"",[44,3188,3189],{"class":96},").status_code)\n",[44,3191,3192],{"class":46,"line":434},[44,3193,110],{"emptyLinePlaceholder":109},[44,3195,3196,3198,3200,3202,3204,3206,3208,3210,3212],{"class":46,"line":454},[44,3197,3093],{"class":96},[44,3199,3096],{"class":149},[44,3201,143],{"class":92},[44,3203,2974],{"class":54},[44,3205,72],{"class":96},[44,3207,2930],{"class":149},[44,3209,143],{"class":92},[44,3211,2935],{"class":54},[44,3213,159],{"class":96},[44,3215,3216,3218,3220,3222,3224,3226,3228,3230,3232,3234,3236,3238,3240,3242,3244],{"class":46,"line":465},[44,3217,3115],{"class":96},[44,3219,143],{"class":92},[44,3221,3120],{"class":96},[44,3223,3123],{"class":54},[44,3225,3126],{"class":96},[44,3227,3129],{"class":54},[44,3229,3132],{"class":96},[44,3231,2592],{"class":54},[44,3233,72],{"class":96},[44,3235,3139],{"class":54},[44,3237,3142],{"class":96},[44,3239,3145],{"class":54},[44,3241,72],{"class":96},[44,3243,3150],{"class":54},[44,3245,3153],{"class":96},[44,3247,3248,3250,3252,3255,3258,3260,3263,3266,3269,3271],{"class":46,"line":470},[44,3249,3158],{"class":155},[44,3251,122],{"class":96},[44,3253,3254],{"class":54},"\"exporter, action:\"",[44,3256,3257],{"class":96},", r.status_code, r[",[44,3259,1628],{"class":54},[44,3261,3262],{"class":96},"].split(",[44,3264,3265],{"class":54},"\";\"",[44,3267,3268],{"class":96},")[",[44,3270,1806],{"class":155},[44,3272,1704],{"class":96},[44,3274,3275,3277,3280,3282],{"class":46,"line":481},[44,3276,1063],{"class":92},[44,3278,3279],{"class":96}," row ",[44,3281,948],{"class":92},[44,3283,3284],{"class":96}," rows(r):\n",[44,3286,3287,3290,3292,3295],{"class":46,"line":859},[44,3288,3289],{"class":155},"    print",[44,3291,122],{"class":96},[44,3293,3294],{"class":54},"\"  \"",[44,3296,3297],{"class":96},", row)\n",[44,3299,3300,3302,3304,3307,3309,3311,3314,3316,3318],{"class":46,"line":902},[44,3301,3115],{"class":96},[44,3303,143],{"class":92},[44,3305,3306],{"class":96}," c.get(",[44,3308,3186],{"class":54},[44,3310,3126],{"class":96},[44,3312,3313],{"class":54},"\"status__exact\"",[44,3315,3132],{"class":96},[44,3317,2538],{"class":54},[44,3319,3320],{"class":96},"})\n",[44,3322,3323,3325,3327,3330,3333,3335,3337,3339,3341,3343,3346,3348],{"class":46,"line":909},[44,3324,3158],{"class":155},[44,3326,122],{"class":96},[44,3328,3329],{"class":54},"\"filtered to paid:\"",[44,3331,3332],{"class":96},", [row[",[44,3334,1806],{"class":155},[44,3336,945],{"class":96},[44,3338,1063],{"class":92},[44,3340,3279],{"class":96},[44,3342,948],{"class":92},[44,3344,3345],{"class":96}," rows(r)[",[44,3347,942],{"class":155},[44,3349,3350],{"class":96},":]])\n",[44,3352,3353,3355,3357,3360,3362,3365,3368,3370,3372],{"class":46,"line":922},[44,3354,3158],{"class":155},[44,3356,122],{"class":96},[44,3358,3359],{"class":54},"\"export button shown:\"",[44,3361,72],{"class":96},[44,3363,3364],{"class":54},"\"Export to Excel\"",[44,3366,3367],{"class":92}," in",[44,3369,3306],{"class":96},[44,3371,3123],{"class":54},[44,3373,3374],{"class":96},").content.decode())\n",[10,3376,3377,3378,72,3380,72,3383,72,3385,72,3387,72,3390,72,3392,3394,3395,3397,3398,3400,3401,3404,3405,3408],{},"The viewer can see orders but gets no Excel file: the action is not in their list (the POST simply returns the changelist page) and the filtered export URL answers 403. The exporter's action returns an attachment whose first row holds the admin headings — ",[20,3379,1965],{},[20,3381,3382],{},"Customer",[20,3384,1976],{},[20,3386,1987],{},[20,3388,3389],{},"Total",[20,3391,1995],{},[20,3393,2013],{}," — followed by the two selected orders with ",[20,3396,1995],{}," and ",[20,3399,2006],{}," instead of codes, times converted from 07:15 UTC to 08:15 London time, and the customer named ",[20,3402,3403],{},"=Café Señor"," safely prefixed with an apostrophe. The filtered export with ",[20,3406,3407],{},"status__exact=paid"," returns both paid orders, regardless of page or selection, and the changelist shows the button. These are the assertions to keep in the project's test suite.",[1784,3410,1793,3415,1793,3418,1793,3421,1793,3424,1793,3427,1793,3431,1793,3437,1793,3440,1793,3444,1793,3447,1793,3451,1793,3455,1793,3460,1793,3462,1793,3466,1793,3468,1793,3471,1793,3473,1793,3476,1793,3479,1793,3483,1793,3485,1793,3487,1793,3489,1793,3492,1793,3494],{"viewBox":3411,"role":1787,"ariaLabelledBy":3412,"xmlns":1791,"style":2697},"4 0 752 140",[3413,3414],"dj-perms-t","dj-perms-d",[1795,3416,3417],{"id":3413},"Who can export what",[1799,3419,3420],{"id":3414},"A staff user with only the view permission sees the changelist but has no export action, and the filtered export URL returns 403. A user who also has the export permission sees the action and the button and receives the workbook.",[1803,3422],{"x":1805,"y":1806,"width":2706,"height":3423,"fill":1809},"140",[1803,3425],{"x":1812,"y":2710,"width":3426,"height":1813,"rx":1806,"fill":1809,"stroke":1817,"style":1818},"191.1",[1803,3428],{"x":3429,"y":2710,"width":3430,"height":1813,"rx":1806,"fill":1959,"stroke":1960,"style":2045},"211.1","176.3",[1824,3432,3436],{"x":3433,"y":3434,"style":3435},"299.2","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","view changelist",[1803,3438],{"x":3439,"y":2710,"width":3430,"height":1813,"rx":1806,"fill":1959,"stroke":1960,"style":2045},"387.4",[1824,3441,3443],{"x":3442,"y":3434,"style":3435},"475.5","export action",[1803,3445],{"x":3446,"y":2710,"width":3430,"height":1813,"rx":1806,"fill":1959,"stroke":1960,"style":2045},"563.7",[1824,3448,3450],{"x":3449,"y":3434,"style":3435},"651.8","filtered export",[1803,3452],{"x":1812,"y":3453,"width":3426,"height":3454,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"56.0","34.0",[1824,3456,3459],{"x":1814,"y":3457,"style":3458},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","viewer (view_order)",[1803,3461],{"x":3429,"y":3453,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3463,3465],{"x":3433,"y":3464,"style":2734},"77.0","✓",[1803,3467],{"x":3439,"y":3453,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3469,3470],{"x":3442,"y":3464,"style":2734},"hidden",[1803,3472],{"x":3446,"y":3453,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3474,3475],{"x":3449,"y":3464,"style":2734},"403",[1803,3477],{"x":1812,"y":3478,"width":3426,"height":3454,"rx":1806,"fill":1816,"stroke":1817,"style":1818},"90.0",[1824,3480,3482],{"x":1814,"y":3481,"style":3458},"112.0","exporter (+ export_order)",[1803,3484],{"x":3429,"y":3478,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3486,3465],{"x":3433,"y":1862,"style":2734},[1803,3488],{"x":3439,"y":3478,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3490,3491],{"x":3442,"y":1862,"style":2734},"✓ xlsx",[1803,3493],{"x":3446,"y":3478,"width":3430,"height":3454,"rx":1806,"fill":1809,"stroke":1817,"style":1818},[1824,3495,3491],{"x":3449,"y":1862,"style":2734},[30,3497,3499],{"id":3498},"common-pitfalls","Common pitfalls",[3501,3502,3503,3513,3519,3525,3531],"ul",{},[2370,3504,3505,3509,3510,3512],{},[3506,3507,3508],"strong",{},"Exporting raw field values."," Choice codes, ",[20,3511,336],{}," and UTC times confuse readers; convert as the admin displays them.",[2370,3514,3515,3518],{},[3506,3516,3517],{},"Only exporting the selected page."," Use the changelist's queryset for \"everything matching\".",[2370,3520,3521,3524],{},[3506,3522,3523],{},"No separate permission."," Viewing a list and downloading all of it are different rights.",[2370,3526,3527,3530],{},[3506,3528,3529],{},"Aware datetimes in openpyxl."," Convert to local time and drop the zone.",[2370,3532,3533,3536],{},[3506,3534,3535],{},"Unbounded exports in a request."," Set a row limit and move big exports to a background job.",[30,3538,3540],{"id":3539},"conclusion","Conclusion",[10,3542,3543,3544,3546,3547,3549,3550,3397,3552,3554,3555,3557,3558,3561],{},"A reusable ",[20,3545,2129],{}," gives every Django admin an Excel export that matches its changelist: columns and headings from ",[20,3548,526],{}," via ",[20,3551,71],{},[20,3553,75],{},", readable choice, boolean and local-time values, and formula-safe text. Add an action for selected rows and an export URL that rebuilds the filtered queryset with ",[20,3556,79],{},", guard both with a dedicated ",[20,3559,3560],{},"export_\u003Cmodel>"," permission and a row limit, and test them with the Django test client as users with and without the permission.",[30,3563,3565],{"id":3564},"frequently-asked-questions","Frequently asked questions",[10,3567,3568,3571],{},[3506,3569,3570],{},"How do I add an export to Excel action to the Django admin?","\nWrite an admin action decorated with admin.action that builds a workbook from the queryset with openpyxl and returns an HttpResponse with the xlsx content type and an attachment Content-Disposition, then list it in the ModelAdmin's actions.",[10,3573,3574,3577],{},[3506,3575,3576],{},"How do I export all filtered rows, not just the selected ones?","\nAdd a URL to the ModelAdmin with get_urls and, in its view, build the queryset with get_changelist_instance and then its get_queryset method, which applies the current filters, search and ordering from the query string.",[10,3579,3580,3583,3584,3586],{},[3506,3581,3582],{},"How do I restrict who can export from the admin?","\nAdd a custom permission in the model's Meta, define has_export_permission on the ModelAdmin, and pass permissions=",[44,3585,1701],{}," to the action decorator. Check the same permission in any export view.",[10,3588,3589,3592],{},[3506,3590,3591],{},"Why are times in my Django Excel export wrong?","\nDjango stores aware UTC datetimes. Convert each to local time with timezone.localtime and remove the tzinfo before writing, since Excel has no time zones.",[30,3594,3596],{"id":3595},"related","Related",[3501,3598,3599,3605,3612,3619],{},[2370,3600,3601,3602,3604],{},"Up one level: ",[14,3603,27],{"href":26}," — Excel downloads from web apps.",[2370,3606,3607,3611],{},[14,3608,3610],{"href":3609},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fimport-an-excel-file-into-django-models\u002F","Import an Excel File into Django Models"," — the reverse direction.",[2370,3613,3614,3618],{},[14,3615,3617],{"href":3616},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002F","Build an Excel Workbook in Memory with BytesIO"," — the in-memory pattern used here.",[2370,3620,3621,3625],{},[14,3622,3624],{"href":3623},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report\u002F","Add a Data Dictionary Sheet to an Excel Report"," — documenting exported columns.",[3627,3628,3629],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}",{"title":40,"searchDepth":106,"depth":106,"links":3631},[3632,3633,3634,3635,3636,3637,3638,3639,3640],{"id":32,"depth":106,"text":33},{"id":519,"depth":106,"text":520},{"id":2054,"depth":106,"text":2055},{"id":2412,"depth":106,"text":2413},{"id":2854,"depth":106,"text":2855},{"id":3498,"depth":106,"text":3499},{"id":3539,"depth":106,"text":3540},{"id":3564,"depth":106,"text":3565},{"id":3595,"depth":106,"text":3596},"2026-10-02","Give every Django admin changelist a proper Excel export: a reusable ModelAdmin mixin that turns list_display into columns with their admin labels, readable choice and boolean values, local dates, an action for selected rows and a button that exports everything matching the current filters, a dedicated export permission, a row limit, and tests with the Django test client.","md",[3645,3647,3649,3651],{"q":3570,"a":3646},"Write an admin action decorated with admin.action that builds a workbook from the queryset with openpyxl and returns an HttpResponse with the xlsx content type and an attachment Content-Disposition, then list it in the ModelAdmin's actions.",{"q":3576,"a":3648},"Add a URL to the ModelAdmin with get_urls and, in its view, build the queryset with get_changelist_instance and then its get_queryset method, which applies the current filters, search and ordering from the query string.",{"q":3582,"a":3650},"Add a custom permission in the model's Meta, define has_export_permission on the ModelAdmin, and pass permissions=[\"export\"] to the action decorator. Check the same permission in any export view.",{"q":3591,"a":3652},"Django stores aware UTC datetimes. Convert each to local time with timezone.localtime and remove the tzinfo before writing, since Excel has no time zones.","add an export to excel action in django admin",null,{"breadcrumb":3656},[3657,3660,3663],{"name":3658,"item":3659},"Home","\u002F",{"name":3661,"item":3662},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":27,"item":26},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin",{"title":3666,"description":3667},"Add an Export to Excel Action in Django Admin with openpyxl","A reusable Django admin mixin for Excel exports: list_display as columns, choice labels and local times, an action for selected rows, export of the filtered changelist, a custom export permission and client tests.","add-an-export-to-excel-action-in-django-admin","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fadd-an-export-to-excel-action-in-django-admin\u002Findex",[3671],"web-apps","how-to","8Wpw8ewYRR6yZPph0W7WlT1cV1JbvQDuOZ5vq-anUSA",[3675,3678],{"title":27,"path":3676,"stem":3677,"children":-1},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Findex",{"title":3679,"path":3680,"stem":3681,"children":-1},"Build a Gradio App That Cleans an Uploaded Excel File","\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-gradio-app-that-cleans-an-uploaded-excel-file","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-gradio-app-that-cleans-an-uploaded-excel-file\u002Findex",[3683,3685],{"path":3684,"title":3661},"\u002Fautomating-reporting-workflows",{"path":3676,"title":27},[],[3688,3691,3694,3695,3698,3700],{"path":3689,"title":3690,"kind":3672},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat","Schedule Excel Reports with Celery Beat",{"path":3692,"title":3693,"kind":3672},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Ftrigger-an-excel-report-from-a-webhook","Trigger an Excel Report from a Webhook",{"path":3680,"title":3679,"kind":3672},{"path":3696,"title":3697,"kind":3672},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-a-streamlit-app-to-upload-and-download-excel","Build a Streamlit App to Upload and Download Excel Files",{"path":3699,"title":3617,"kind":3672},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio",{"path":3701,"title":3702,"kind":3672},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fcache-generated-excel-files-in-a-web-app","Cache Generated Excel Files in a Web App",1790972140271]