[{"data":1,"prerenderedAt":2676},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel":3,"surround:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel":2667},{"id":4,"title":5,"body":6,"dateModified":2644,"datePublished":2644,"description":2645,"extension":2646,"faq":2647,"meta":2658,"navigation":353,"path":2659,"seo":2660,"slug":2663,"stem":2664,"type":2665,"__hash__":2666},"docs\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002Findex.md","Export a Django Queryset to Excel",{"type":7,"value":8,"toc":2628},"minimark",[9,19,144,149,183,193,197,203,312,318,420,424,427,1031,1046,1050,1245,1260,1317,1321,1330,1542,1547,1551,1554,1774,1780,1784,1798,1881,1888,1892,2142,2145,2149,2152,2356,2362,2437,2441,2496,2500,2517,2521,2530,2534,2542,2558,2567,2576,2582,2586,2624],[10,11,12,13,18],"p",{},"A Django export view has two halves: getting the right rows out of the ORM efficiently, and turning them into a workbook a person can use. Both halves have a wrong way that works fine on a hundred rows and falls over on a hundred thousand — iterating model instances, and building the file on disk. This guide does both properly, adds an admin action for bulk exports, and covers the permission checks an export endpoint deserves. It is the Django branch of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002F","Serving Excel Files from Python Web Apps",".",[20,21,29,30,29,34,29,38,29,45,29,52,29,61,29,67,29,72,29,80,29,85,29,89,29,94,29,97,29,101,29,104,29,108,29,113,29,117,29,122,29,126,29,131,29,136,29,140],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 226","img",[25,26],"dj-t","dj-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"From queryset to download, without model instances",[35,36,37],"desc",{"id":26},"A scoped queryset with select_related and values yields dictionaries, which are written straight into an in-memory workbook and returned as an attachment response.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","226","#ffffff",[46,47,51],"text",{"x":48,"y":49,"style":50},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","Rows, not objects",[39,53],{"x":54,"y":55,"width":56,"height":57,"rx":58,"fill":59,"stroke":60},"24","72","164","76","12","#f0f2f5","var(--line,#cdd5e6)",[46,62,66],{"x":63,"y":64,"style":65},"106","104","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","queryset",[46,68,71],{"x":63,"y":69,"style":70},"126","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","scoped to the user",[73,74],"line",{"x1":75,"y1":76,"x2":77,"y2":76,"stroke":78,"style":79},"188","110","220","var(--brand,#5b5cf0)","stroke-width:2px",[81,82],"polygon",{"points":83,"fill":84},"220,110 210,105 210,115","#5b5cf0",[39,86],{"x":87,"y":55,"width":56,"height":57,"rx":58,"fill":88,"stroke":60},"224","#ebebfd",[46,90,93],{"x":91,"y":64,"style":92},"306","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",".values(...)",[46,95,96],{"x":91,"y":69,"style":70},"dicts from the DB",[73,98],{"x1":99,"y1":76,"x2":100,"y2":76,"stroke":78,"style":79},"388","420",[81,102],{"points":103,"fill":84},"420,110 410,105 410,115",[39,105],{"x":106,"y":55,"width":56,"height":57,"rx":58,"fill":107,"stroke":60},"424","#fdefd8",[46,109,112],{"x":110,"y":64,"style":111},"506","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","workbook",[46,114,116],{"x":110,"y":69,"style":115},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","built in BytesIO",[73,118],{"x1":119,"y1":76,"x2":120,"y2":76,"stroke":121,"style":79},"588","620","var(--teal,#0f9488)",[81,123],{"points":124,"fill":125},"620,110 610,105 610,115","#0f766e",[39,127],{"x":128,"y":55,"width":129,"height":57,"rx":58,"fill":130,"stroke":60},"624","112","#d9f4f1",[46,132,135],{"x":133,"y":64,"style":134},"680","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","HttpResponse",[46,137,139],{"x":133,"y":69,"style":138},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","attachment",[46,141,143],{"x":48,"y":142,"style":70},"200","Building model instances for an export is work you throw away one field later",[145,146,148],"h2",{"id":147},"prerequisites","Prerequisites",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install django pandas xlsxwriter openpyxl\n","bash","",[157,158,159],"code",{"__ignoreMap":155},[160,161,163,167,171,174,177,180],"span",{"class":73,"line":162},1,[160,164,166],{"class":165},"sMTad","pip",[160,168,170],{"class":169},"srMev"," install",[160,172,173],{"class":169}," django",[160,175,176],{"class":169}," pandas",[160,178,179],{"class":169}," xlsxwriter",[160,181,182],{"class":169}," openpyxl\n",[10,184,185,186,189,190,18],{},"The examples assume a familiar shape: an ",[157,187,188],{},"Order"," model with a foreign key to ",[157,191,192],{},"Customer",[145,194,196],{"id":195},"query-the-columns-not-the-objects","Query the columns, not the objects",[10,198,199,202],{},[157,200,201],{},"values()"," returns dictionaries straight from the database. No model instances are constructed, and double-underscore paths pull in related fields in the same query:",[150,204,208],{"className":205,"code":206,"language":207,"meta":155,"style":155},"language-python shiki shiki-themes github-light github-dark-high-contrast","rows = (\n    Order.objects\n    .filter(owner=request.user, created__gte=start)\n    .select_related(\"customer\")\n    .values(\"reference\", \"created\", \"customer__name\", \"region\", \"total\")\n    .order_by(\"-created\")\n)\n","python",[157,209,210,223,229,252,264,296,307],{"__ignoreMap":155},[160,211,212,216,220],{"class":73,"line":162},[160,213,215],{"class":214},"skGVy","rows ",[160,217,219],{"class":218},"s-kum","=",[160,221,222],{"class":214}," (\n",[160,224,226],{"class":73,"line":225},2,[160,227,228],{"class":214},"    Order.objects\n",[160,230,232,235,239,241,244,247,249],{"class":73,"line":231},3,[160,233,234],{"class":214},"    .filter(",[160,236,238],{"class":237},"sa561","owner",[160,240,219],{"class":218},[160,242,243],{"class":214},"request.user, ",[160,245,246],{"class":237},"created__gte",[160,248,219],{"class":218},[160,250,251],{"class":214},"start)\n",[160,253,255,258,261],{"class":73,"line":254},4,[160,256,257],{"class":214},"    .select_related(",[160,259,260],{"class":169},"\"customer\"",[160,262,263],{"class":214},")\n",[160,265,267,270,273,276,279,281,284,286,289,291,294],{"class":73,"line":266},5,[160,268,269],{"class":214},"    .values(",[160,271,272],{"class":169},"\"reference\"",[160,274,275],{"class":214},", ",[160,277,278],{"class":169},"\"created\"",[160,280,275],{"class":214},[160,282,283],{"class":169},"\"customer__name\"",[160,285,275],{"class":214},[160,287,288],{"class":169},"\"region\"",[160,290,275],{"class":214},[160,292,293],{"class":169},"\"total\"",[160,295,263],{"class":214},[160,297,299,302,305],{"class":73,"line":298},6,[160,300,301],{"class":214},"    .order_by(",[160,303,304],{"class":169},"\"-created\"",[160,306,263],{"class":214},[160,308,310],{"class":73,"line":309},7,[160,311,263],{"class":214},[10,313,314,317],{},[157,315,316],{},"select_related(\"customer\")"," turns what would be one query per row into a single join — the N+1 problem that makes an export mysteriously slow in proportion to its size. If a column needs computing, do it in the database rather than in Python:",[150,319,321],{"className":205,"code":320,"language":207,"meta":155,"style":155},"from django.db.models import F, Value\nfrom django.db.models.functions import Concat\n\nrows = rows.annotate(\n    net=F(\"total\") - F(\"discount\"),\n    label=Concat(\"region\", Value(\" \u002F \"), \"customer__name\"),\n)\n",[157,322,323,337,349,355,364,391,416],{"__ignoreMap":155},[160,324,325,328,331,334],{"class":73,"line":162},[160,326,327],{"class":218},"from",[160,329,330],{"class":214}," django.db.models ",[160,332,333],{"class":218},"import",[160,335,336],{"class":214}," F, Value\n",[160,338,339,341,344,346],{"class":73,"line":225},[160,340,327],{"class":218},[160,342,343],{"class":214}," django.db.models.functions ",[160,345,333],{"class":218},[160,347,348],{"class":214}," Concat\n",[160,350,351],{"class":73,"line":231},[160,352,354],{"emptyLinePlaceholder":353},true,"\n",[160,356,357,359,361],{"class":73,"line":254},[160,358,215],{"class":214},[160,360,219],{"class":218},[160,362,363],{"class":214}," rows.annotate(\n",[160,365,366,369,371,374,376,379,382,385,388],{"class":73,"line":266},[160,367,368],{"class":237},"    net",[160,370,219],{"class":218},[160,372,373],{"class":214},"F(",[160,375,293],{"class":169},[160,377,378],{"class":214},") ",[160,380,381],{"class":218},"-",[160,383,384],{"class":214}," F(",[160,386,387],{"class":169},"\"discount\"",[160,389,390],{"class":214},"),\n",[160,392,393,396,398,401,403,406,409,412,414],{"class":73,"line":298},[160,394,395],{"class":237},"    label",[160,397,219],{"class":218},[160,399,400],{"class":214},"Concat(",[160,402,288],{"class":169},[160,404,405],{"class":214},", Value(",[160,407,408],{"class":169},"\" \u002F \"",[160,410,411],{"class":214},"), ",[160,413,283],{"class":169},[160,415,390],{"class":214},[160,417,418],{"class":73,"line":309},[160,419,263],{"class":214},[145,421,423],{"id":422},"build-the-workbook","Build the workbook",[10,425,426],{},"Keep the builder separate from the view so an admin action and a background job can reuse it:",[150,428,430],{"className":205,"code":429,"language":207,"meta":155,"style":155},"\"\"\"exports.py\"\"\"\nimport io\n\nimport pandas as pd\n\nXLSX = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\nHEADERS = {\n    \"reference\": \"Reference\",\n    \"created\": \"Created\",\n    \"customer__name\": \"Customer\",\n    \"region\": \"Region\",\n    \"total\": \"Total\",\n}\n\ndef queryset_to_xlsx(rows, sheet: str = \"Orders\") -> bytes:\n    df = pd.DataFrame.from_records(list(rows))\n    if df.empty:\n        df = pd.DataFrame({\"message\": [\"No rows matched the selected filters.\"]})\n    else:\n        df = df.rename(columns=HEADERS)[list(HEADERS.values())]\n        df[\"Created\"] = pd.to_datetime(df[\"Created\"]).dt.tz_localize(None)\n\n    buffer = io.BytesIO()\n    with pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, index=False, sheet_name=sheet)\n        book, ws = writer.book, writer.sheets[sheet]\n        header = book.add_format({\"bold\": True, \"bg_color\": \"#DDEBF7\", \"border\": 1})\n        money = book.add_format({\"num_format\": \"#,##0.00\"})\n        date = book.add_format({\"num_format\": \"yyyy-mm-dd\"})\n\n        for col, name in enumerate(df.columns):\n            ws.write(0, col, name, header)\n            fmt = money if name == \"Total\" else date if name == \"Created\" else None\n            ws.set_column(col, col, max(len(name) + 2, 14), fmt)\n        ws.freeze_panes(1, 0)\n        ws.autofilter(0, 0, len(df), len(df.columns) - 1)\n    return buffer.getvalue()\n",[157,431,432,437,444,448,461,465,477,481,492,507,520,533,546,559,565,570,600,617,626,649,657,687,713,718,729,753,777,788,830,850,869,874,892,903,946,977,991,1022],{"__ignoreMap":155},[160,433,434],{"class":73,"line":162},[160,435,436],{"class":169},"\"\"\"exports.py\"\"\"\n",[160,438,439,441],{"class":73,"line":225},[160,440,333],{"class":218},[160,442,443],{"class":214}," io\n",[160,445,446],{"class":73,"line":231},[160,447,354],{"emptyLinePlaceholder":353},[160,449,450,452,455,458],{"class":73,"line":254},[160,451,333],{"class":218},[160,453,454],{"class":214}," pandas ",[160,456,457],{"class":218},"as",[160,459,460],{"class":214}," pd\n",[160,462,463],{"class":73,"line":266},[160,464,354],{"emptyLinePlaceholder":353},[160,466,467,471,474],{"class":73,"line":298},[160,468,470],{"class":469},"sP0c6","XLSX",[160,472,473],{"class":218}," =",[160,475,476],{"class":169}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[160,478,479],{"class":73,"line":309},[160,480,354],{"emptyLinePlaceholder":353},[160,482,484,487,489],{"class":73,"line":483},8,[160,485,486],{"class":469},"HEADERS",[160,488,473],{"class":218},[160,490,491],{"class":214}," {\n",[160,493,495,498,501,504],{"class":73,"line":494},9,[160,496,497],{"class":169},"    \"reference\"",[160,499,500],{"class":214},": ",[160,502,503],{"class":169},"\"Reference\"",[160,505,506],{"class":214},",\n",[160,508,510,513,515,518],{"class":73,"line":509},10,[160,511,512],{"class":169},"    \"created\"",[160,514,500],{"class":214},[160,516,517],{"class":169},"\"Created\"",[160,519,506],{"class":214},[160,521,523,526,528,531],{"class":73,"line":522},11,[160,524,525],{"class":169},"    \"customer__name\"",[160,527,500],{"class":214},[160,529,530],{"class":169},"\"Customer\"",[160,532,506],{"class":214},[160,534,536,539,541,544],{"class":73,"line":535},12,[160,537,538],{"class":169},"    \"region\"",[160,540,500],{"class":214},[160,542,543],{"class":169},"\"Region\"",[160,545,506],{"class":214},[160,547,549,552,554,557],{"class":73,"line":548},13,[160,550,551],{"class":169},"    \"total\"",[160,553,500],{"class":214},[160,555,556],{"class":169},"\"Total\"",[160,558,506],{"class":214},[160,560,562],{"class":73,"line":561},14,[160,563,564],{"class":214},"}\n",[160,566,568],{"class":73,"line":567},15,[160,569,354],{"emptyLinePlaceholder":353},[160,571,573,576,580,583,586,588,591,594,597],{"class":73,"line":572},16,[160,574,575],{"class":218},"def",[160,577,579],{"class":578},"s_Opv"," queryset_to_xlsx",[160,581,582],{"class":214},"(rows, sheet: ",[160,584,585],{"class":469},"str",[160,587,473],{"class":218},[160,589,590],{"class":169}," \"Orders\"",[160,592,593],{"class":214},") -> ",[160,595,596],{"class":469},"bytes",[160,598,599],{"class":214},":\n",[160,601,603,606,608,611,614],{"class":73,"line":602},17,[160,604,605],{"class":214},"    df ",[160,607,219],{"class":218},[160,609,610],{"class":214}," pd.DataFrame.from_records(",[160,612,613],{"class":469},"list",[160,615,616],{"class":214},"(rows))\n",[160,618,620,623],{"class":73,"line":619},18,[160,621,622],{"class":218},"    if",[160,624,625],{"class":214}," df.empty:\n",[160,627,629,632,634,637,640,643,646],{"class":73,"line":628},19,[160,630,631],{"class":214},"        df ",[160,633,219],{"class":218},[160,635,636],{"class":214}," pd.DataFrame({",[160,638,639],{"class":169},"\"message\"",[160,641,642],{"class":214},": [",[160,644,645],{"class":169},"\"No rows matched the selected filters.\"",[160,647,648],{"class":214},"]})\n",[160,650,652,655],{"class":73,"line":651},20,[160,653,654],{"class":218},"    else",[160,656,599],{"class":214},[160,658,660,662,664,667,670,672,674,677,679,682,684],{"class":73,"line":659},21,[160,661,631],{"class":214},[160,663,219],{"class":218},[160,665,666],{"class":214}," df.rename(",[160,668,669],{"class":237},"columns",[160,671,219],{"class":218},[160,673,486],{"class":469},[160,675,676],{"class":214},")[",[160,678,613],{"class":469},[160,680,681],{"class":214},"(",[160,683,486],{"class":469},[160,685,686],{"class":214},".values())]\n",[160,688,690,693,695,698,700,703,705,708,711],{"class":73,"line":689},22,[160,691,692],{"class":214},"        df[",[160,694,517],{"class":169},[160,696,697],{"class":214},"] ",[160,699,219],{"class":218},[160,701,702],{"class":214}," pd.to_datetime(df[",[160,704,517],{"class":169},[160,706,707],{"class":214},"]).dt.tz_localize(",[160,709,710],{"class":469},"None",[160,712,263],{"class":214},[160,714,716],{"class":73,"line":715},23,[160,717,354],{"emptyLinePlaceholder":353},[160,719,721,724,726],{"class":73,"line":720},24,[160,722,723],{"class":214},"    buffer ",[160,725,219],{"class":218},[160,727,728],{"class":214}," io.BytesIO()\n",[160,730,732,735,738,741,743,746,748,750],{"class":73,"line":731},25,[160,733,734],{"class":218},"    with",[160,736,737],{"class":214}," pd.ExcelWriter(buffer, ",[160,739,740],{"class":237},"engine",[160,742,219],{"class":218},[160,744,745],{"class":169},"\"xlsxwriter\"",[160,747,378],{"class":214},[160,749,457],{"class":218},[160,751,752],{"class":214}," writer:\n",[160,754,756,759,762,764,767,769,772,774],{"class":73,"line":755},26,[160,757,758],{"class":214},"        df.to_excel(writer, ",[160,760,761],{"class":237},"index",[160,763,219],{"class":218},[160,765,766],{"class":469},"False",[160,768,275],{"class":214},[160,770,771],{"class":237},"sheet_name",[160,773,219],{"class":218},[160,775,776],{"class":214},"sheet)\n",[160,778,780,783,785],{"class":73,"line":779},27,[160,781,782],{"class":214},"        book, ws ",[160,784,219],{"class":218},[160,786,787],{"class":214}," writer.book, writer.sheets[sheet]\n",[160,789,791,794,796,799,802,804,807,809,812,814,817,819,822,824,827],{"class":73,"line":790},28,[160,792,793],{"class":214},"        header ",[160,795,219],{"class":218},[160,797,798],{"class":214}," book.add_format({",[160,800,801],{"class":169},"\"bold\"",[160,803,500],{"class":214},[160,805,806],{"class":469},"True",[160,808,275],{"class":214},[160,810,811],{"class":169},"\"bg_color\"",[160,813,500],{"class":214},[160,815,816],{"class":169},"\"#DDEBF7\"",[160,818,275],{"class":214},[160,820,821],{"class":169},"\"border\"",[160,823,500],{"class":214},[160,825,826],{"class":469},"1",[160,828,829],{"class":214},"})\n",[160,831,833,836,838,840,843,845,848],{"class":73,"line":832},29,[160,834,835],{"class":214},"        money ",[160,837,219],{"class":218},[160,839,798],{"class":214},[160,841,842],{"class":169},"\"num_format\"",[160,844,500],{"class":214},[160,846,847],{"class":169},"\"#,##0.00\"",[160,849,829],{"class":214},[160,851,853,856,858,860,862,864,867],{"class":73,"line":852},30,[160,854,855],{"class":214},"        date ",[160,857,219],{"class":218},[160,859,798],{"class":214},[160,861,842],{"class":169},[160,863,500],{"class":214},[160,865,866],{"class":169},"\"yyyy-mm-dd\"",[160,868,829],{"class":214},[160,870,872],{"class":73,"line":871},31,[160,873,354],{"emptyLinePlaceholder":353},[160,875,877,880,883,886,889],{"class":73,"line":876},32,[160,878,879],{"class":218},"        for",[160,881,882],{"class":214}," col, name ",[160,884,885],{"class":218},"in",[160,887,888],{"class":469}," enumerate",[160,890,891],{"class":214},"(df.columns):\n",[160,893,895,898,900],{"class":73,"line":894},33,[160,896,897],{"class":214},"            ws.write(",[160,899,41],{"class":469},[160,901,902],{"class":214},", col, name, header)\n",[160,904,906,909,911,914,917,920,923,926,929,932,934,936,938,941,943],{"class":73,"line":905},34,[160,907,908],{"class":214},"            fmt ",[160,910,219],{"class":218},[160,912,913],{"class":214}," money ",[160,915,916],{"class":218},"if",[160,918,919],{"class":214}," name ",[160,921,922],{"class":218},"==",[160,924,925],{"class":169}," \"Total\"",[160,927,928],{"class":218}," else",[160,930,931],{"class":214}," date ",[160,933,916],{"class":218},[160,935,919],{"class":214},[160,937,922],{"class":218},[160,939,940],{"class":169}," \"Created\"",[160,942,928],{"class":218},[160,944,945],{"class":469}," None\n",[160,947,949,952,955,957,960,963,966,969,971,974],{"class":73,"line":948},35,[160,950,951],{"class":214},"            ws.set_column(col, col, ",[160,953,954],{"class":469},"max",[160,956,681],{"class":214},[160,958,959],{"class":469},"len",[160,961,962],{"class":214},"(name) ",[160,964,965],{"class":218},"+",[160,967,968],{"class":469}," 2",[160,970,275],{"class":214},[160,972,973],{"class":469},"14",[160,975,976],{"class":214},"), fmt)\n",[160,978,980,983,985,987,989],{"class":73,"line":979},36,[160,981,982],{"class":214},"        ws.freeze_panes(",[160,984,826],{"class":469},[160,986,275],{"class":214},[160,988,41],{"class":469},[160,990,263],{"class":214},[160,992,994,997,999,1001,1003,1005,1007,1010,1012,1015,1017,1020],{"class":73,"line":993},37,[160,995,996],{"class":214},"        ws.autofilter(",[160,998,41],{"class":469},[160,1000,275],{"class":214},[160,1002,41],{"class":469},[160,1004,275],{"class":214},[160,1006,959],{"class":469},[160,1008,1009],{"class":214},"(df), ",[160,1011,959],{"class":469},[160,1013,1014],{"class":214},"(df.columns) ",[160,1016,381],{"class":218},[160,1018,1019],{"class":469}," 1",[160,1021,263],{"class":214},[160,1023,1025,1028],{"class":73,"line":1024},38,[160,1026,1027],{"class":218},"    return",[160,1029,1030],{"class":214}," buffer.getvalue()\n",[10,1032,1033,1034,1037,1038,1041,1042,18],{},"The ",[157,1035,1036],{},"tz_localize(None)"," line is not optional. Django stores timezone-aware datetimes when ",[157,1039,1040],{},"USE_TZ"," is on, and Excel has no way to represent an offset — openpyxl raises and xlsxwriter writes something misleading. Convert to the display timezone first if that matters, as covered in ",[14,1043,1045],{"href":1044},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002F","Handle timezones in Excel timestamps with Python",[145,1047,1049],{"id":1048},"return-it-from-a-view","Return it from a view",[150,1051,1053],{"className":205,"code":1052,"language":207,"meta":155,"style":155},"\"\"\"views.py\"\"\"\nfrom django.contrib.auth.decorators import login_required, permission_required\nfrom django.http import HttpResponse\n\nfrom .exports import XLSX, queryset_to_xlsx\n\n@login_required\n@permission_required(\"orders.view_order\", raise_exception=True)\ndef export_orders(request):\n    rows = (Order.objects\n            .filter(owner=request.user)\n            .select_related(\"customer\")\n            .values(\"reference\", \"created\", \"customer__name\", \"region\", \"total\"))\n\n    response = HttpResponse(queryset_to_xlsx(rows), content_type=XLSX)\n    response[\"Content-Disposition\"] = 'attachment; filename=\"orders.xlsx\"'\n    return response\n",[157,1054,1055,1060,1072,1084,1088,1103,1107,1112,1133,1143,1153,1165,1174,1200,1204,1223,1238],{"__ignoreMap":155},[160,1056,1057],{"class":73,"line":162},[160,1058,1059],{"class":169},"\"\"\"views.py\"\"\"\n",[160,1061,1062,1064,1067,1069],{"class":73,"line":225},[160,1063,327],{"class":218},[160,1065,1066],{"class":214}," django.contrib.auth.decorators ",[160,1068,333],{"class":218},[160,1070,1071],{"class":214}," login_required, permission_required\n",[160,1073,1074,1076,1079,1081],{"class":73,"line":231},[160,1075,327],{"class":218},[160,1077,1078],{"class":214}," django.http ",[160,1080,333],{"class":218},[160,1082,1083],{"class":214}," HttpResponse\n",[160,1085,1086],{"class":73,"line":254},[160,1087,354],{"emptyLinePlaceholder":353},[160,1089,1090,1092,1095,1097,1100],{"class":73,"line":266},[160,1091,327],{"class":218},[160,1093,1094],{"class":214}," .exports ",[160,1096,333],{"class":218},[160,1098,1099],{"class":469}," XLSX",[160,1101,1102],{"class":214},", queryset_to_xlsx\n",[160,1104,1105],{"class":73,"line":298},[160,1106,354],{"emptyLinePlaceholder":353},[160,1108,1109],{"class":73,"line":309},[160,1110,1111],{"class":578},"@login_required\n",[160,1113,1114,1117,1119,1122,1124,1127,1129,1131],{"class":73,"line":483},[160,1115,1116],{"class":578},"@permission_required",[160,1118,681],{"class":214},[160,1120,1121],{"class":169},"\"orders.view_order\"",[160,1123,275],{"class":214},[160,1125,1126],{"class":237},"raise_exception",[160,1128,219],{"class":218},[160,1130,806],{"class":469},[160,1132,263],{"class":214},[160,1134,1135,1137,1140],{"class":73,"line":494},[160,1136,575],{"class":218},[160,1138,1139],{"class":578}," export_orders",[160,1141,1142],{"class":214},"(request):\n",[160,1144,1145,1148,1150],{"class":73,"line":509},[160,1146,1147],{"class":214},"    rows ",[160,1149,219],{"class":218},[160,1151,1152],{"class":214}," (Order.objects\n",[160,1154,1155,1158,1160,1162],{"class":73,"line":522},[160,1156,1157],{"class":214},"            .filter(",[160,1159,238],{"class":237},[160,1161,219],{"class":218},[160,1163,1164],{"class":214},"request.user)\n",[160,1166,1167,1170,1172],{"class":73,"line":535},[160,1168,1169],{"class":214},"            .select_related(",[160,1171,260],{"class":169},[160,1173,263],{"class":214},[160,1175,1176,1179,1181,1183,1185,1187,1189,1191,1193,1195,1197],{"class":73,"line":548},[160,1177,1178],{"class":214},"            .values(",[160,1180,272],{"class":169},[160,1182,275],{"class":214},[160,1184,278],{"class":169},[160,1186,275],{"class":214},[160,1188,283],{"class":169},[160,1190,275],{"class":214},[160,1192,288],{"class":169},[160,1194,275],{"class":214},[160,1196,293],{"class":169},[160,1198,1199],{"class":214},"))\n",[160,1201,1202],{"class":73,"line":561},[160,1203,354],{"emptyLinePlaceholder":353},[160,1205,1206,1209,1211,1214,1217,1219,1221],{"class":73,"line":567},[160,1207,1208],{"class":214},"    response ",[160,1210,219],{"class":218},[160,1212,1213],{"class":214}," HttpResponse(queryset_to_xlsx(rows), ",[160,1215,1216],{"class":237},"content_type",[160,1218,219],{"class":218},[160,1220,470],{"class":469},[160,1222,263],{"class":214},[160,1224,1225,1228,1231,1233,1235],{"class":73,"line":572},[160,1226,1227],{"class":214},"    response[",[160,1229,1230],{"class":169},"\"Content-Disposition\"",[160,1232,697],{"class":214},[160,1234,219],{"class":218},[160,1236,1237],{"class":169}," 'attachment; filename=\"orders.xlsx\"'\n",[160,1239,1240,1242],{"class":73,"line":602},[160,1241,1027],{"class":218},[160,1243,1244],{"class":214}," response\n",[10,1246,1247,1248,1251,1252,1255,1256,1259],{},"Two decorators do a lot of work here: ",[157,1249,1250],{},"login_required"," blocks anonymous access, and ",[157,1253,1254],{},"permission_required"," makes the export subject to the same permission as viewing the model. The ",[157,1257,1258],{},"filter(owner=request.user)"," is the row-level half — a permission alone does not stop one user's export returning another's rows.",[20,1261,29,1266,29,1269,29,1272,29,1275,29,1278,29,1284,29,1289,29,1294,29,1298,29,1302,29,1305,29,1309,29,1312,29,1314],{"viewBox":1262,"role":23,"ariaLabelledBy":1263,"xmlns":27,"style":28},"0 0 760 218",[1264,1265],"dj2-t","dj2-d",[31,1267,1268],{"id":1264},"Two independent checks an export needs",[35,1270,1271],{"id":1265},"A model permission decides whether the user may export this kind of record at all, while a queryset filter decides which records they may see; both are required.",[39,1273],{"x":41,"y":41,"width":42,"height":1274,"fill":44},"218",[46,1276,1277],{"x":48,"y":49,"style":50},"Permission and scope answer different questions",[39,1279],{"x":1280,"y":1281,"width":1282,"height":1283,"rx":58,"fill":88,"stroke":60},"30","46","332","132",[46,1285,1288],{"x":1286,"y":1287,"style":92},"196","78","model permission",[46,1290,1293],{"x":1286,"y":1291,"style":1292},"108","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","may this user export orders?",[46,1295,1297],{"x":1286,"y":1296,"style":70},"134","permission_required decorator",[46,1299,1301],{"x":1286,"y":1300,"style":70},"160","answers yes or no once",[39,1303],{"x":1304,"y":1281,"width":1282,"height":1283,"rx":58,"fill":130,"stroke":60},"398",[46,1306,1308],{"x":1307,"y":1287,"style":134},"564","queryset scope",[46,1310,1311],{"x":1307,"y":1291,"style":1292},"which orders may they see?",[46,1313,1258],{"x":1307,"y":1296,"style":138},[46,1315,1316],{"x":1307,"y":1300,"style":138},"answers per row, every time",[145,1318,1320],{"id":1319},"keep-memory-flat-on-a-large-export","Keep memory flat on a large export",[10,1322,1323,1325,1326,1329],{},[157,1324,201],{}," still materialises every dictionary. For a large queryset, ",[157,1327,1328],{},"iterator()"," streams from the database cursor instead, and writing rows directly with xlsxwriter avoids the DataFrame entirely:",[150,1331,1333],{"className":205,"code":1332,"language":207,"meta":155,"style":155},"import io\n\nimport xlsxwriter\n\ndef stream_orders_to_xlsx(queryset, columns: dict[str, str]) -> bytes:\n    buffer = io.BytesIO()\n    with xlsxwriter.Workbook(buffer, {\"in_memory\": True}) as book:\n        ws = book.add_worksheet(\"Orders\")\n        bold = book.add_format({\"bold\": True})\n        ws.write_row(0, 0, list(columns.values()), bold)\n\n        for r, row in enumerate(queryset.values(*columns).iterator(chunk_size=2_000), start=1):\n            ws.write_row(r, 0, [row[key] for key in columns])\n        ws.freeze_panes(1, 0)\n    return buffer.getvalue()\n",[157,1334,1335,1341,1345,1352,1356,1379,1387,1409,1424,1441,1459,1463,1503,1524,1536],{"__ignoreMap":155},[160,1336,1337,1339],{"class":73,"line":162},[160,1338,333],{"class":218},[160,1340,443],{"class":214},[160,1342,1343],{"class":73,"line":225},[160,1344,354],{"emptyLinePlaceholder":353},[160,1346,1347,1349],{"class":73,"line":231},[160,1348,333],{"class":218},[160,1350,1351],{"class":214}," xlsxwriter\n",[160,1353,1354],{"class":73,"line":254},[160,1355,354],{"emptyLinePlaceholder":353},[160,1357,1358,1360,1363,1366,1368,1370,1372,1375,1377],{"class":73,"line":266},[160,1359,575],{"class":218},[160,1361,1362],{"class":578}," stream_orders_to_xlsx",[160,1364,1365],{"class":214},"(queryset, columns: dict[",[160,1367,585],{"class":469},[160,1369,275],{"class":214},[160,1371,585],{"class":469},[160,1373,1374],{"class":214},"]) -> ",[160,1376,596],{"class":469},[160,1378,599],{"class":214},[160,1380,1381,1383,1385],{"class":73,"line":298},[160,1382,723],{"class":214},[160,1384,219],{"class":218},[160,1386,728],{"class":214},[160,1388,1389,1391,1394,1397,1399,1401,1404,1406],{"class":73,"line":309},[160,1390,734],{"class":218},[160,1392,1393],{"class":214}," xlsxwriter.Workbook(buffer, {",[160,1395,1396],{"class":169},"\"in_memory\"",[160,1398,500],{"class":214},[160,1400,806],{"class":469},[160,1402,1403],{"class":214},"}) ",[160,1405,457],{"class":218},[160,1407,1408],{"class":214}," book:\n",[160,1410,1411,1414,1416,1419,1422],{"class":73,"line":483},[160,1412,1413],{"class":214},"        ws ",[160,1415,219],{"class":218},[160,1417,1418],{"class":214}," book.add_worksheet(",[160,1420,1421],{"class":169},"\"Orders\"",[160,1423,263],{"class":214},[160,1425,1426,1429,1431,1433,1435,1437,1439],{"class":73,"line":494},[160,1427,1428],{"class":214},"        bold ",[160,1430,219],{"class":218},[160,1432,798],{"class":214},[160,1434,801],{"class":169},[160,1436,500],{"class":214},[160,1438,806],{"class":469},[160,1440,829],{"class":214},[160,1442,1443,1446,1448,1450,1452,1454,1456],{"class":73,"line":509},[160,1444,1445],{"class":214},"        ws.write_row(",[160,1447,41],{"class":469},[160,1449,275],{"class":214},[160,1451,41],{"class":469},[160,1453,275],{"class":214},[160,1455,613],{"class":469},[160,1457,1458],{"class":214},"(columns.values()), bold)\n",[160,1460,1461],{"class":73,"line":522},[160,1462,354],{"emptyLinePlaceholder":353},[160,1464,1465,1467,1470,1472,1474,1477,1480,1483,1486,1488,1491,1493,1496,1498,1500],{"class":73,"line":535},[160,1466,879],{"class":218},[160,1468,1469],{"class":214}," r, row ",[160,1471,885],{"class":218},[160,1473,888],{"class":469},[160,1475,1476],{"class":214},"(queryset.values(",[160,1478,1479],{"class":218},"*",[160,1481,1482],{"class":214},"columns).iterator(",[160,1484,1485],{"class":237},"chunk_size",[160,1487,219],{"class":218},[160,1489,1490],{"class":469},"2_000",[160,1492,411],{"class":214},[160,1494,1495],{"class":237},"start",[160,1497,219],{"class":218},[160,1499,826],{"class":469},[160,1501,1502],{"class":214},"):\n",[160,1504,1505,1508,1510,1513,1516,1519,1521],{"class":73,"line":548},[160,1506,1507],{"class":214},"            ws.write_row(r, ",[160,1509,41],{"class":469},[160,1511,1512],{"class":214},", [row[key] ",[160,1514,1515],{"class":218},"for",[160,1517,1518],{"class":214}," key ",[160,1520,885],{"class":218},[160,1522,1523],{"class":214}," columns])\n",[160,1525,1526,1528,1530,1532,1534],{"class":73,"line":561},[160,1527,982],{"class":214},[160,1529,826],{"class":469},[160,1531,275],{"class":214},[160,1533,41],{"class":469},[160,1535,263],{"class":214},[160,1537,1538,1540],{"class":73,"line":567},[160,1539,1027],{"class":218},[160,1541,1030],{"class":214},[10,1543,1544,1546],{},[157,1545,1485],{}," controls how many rows the database driver fetches at a time; a few thousand is a reasonable default. Beyond a few hundred thousand rows, stop serving inline altogether — queue the job, store the file, and hand back a link, as described in the parent topic.",[145,1548,1550],{"id":1549},"add-an-admin-action","Add an admin action",[10,1552,1553],{},"The same builder gives the Django admin a bulk export on any changelist:",[150,1555,1557],{"className":205,"code":1556,"language":207,"meta":155,"style":155},"\"\"\"admin.py\"\"\"\nfrom django.contrib import admin\nfrom django.http import HttpResponse\n\nfrom .exports import XLSX, queryset_to_xlsx\n\n@admin.action(description=\"Export selected orders to Excel\")\ndef export_selected(modeladmin, request, queryset):\n    rows = queryset.select_related(\"customer\").values(\n        \"reference\", \"created\", \"customer__name\", \"region\", \"total\")\n    response = HttpResponse(queryset_to_xlsx(rows), content_type=XLSX)\n    response[\"Content-Disposition\"] = 'attachment; filename=\"orders-selected.xlsx\"'\n    return response\n\n@admin.register(Order)\nclass OrderAdmin(admin.ModelAdmin):\n    list_display = (\"reference\", \"customer\", \"region\", \"total\")\n    actions = [export_selected]\n",[157,1558,1559,1564,1576,1586,1590,1602,1606,1623,1633,1647,1670,1686,1699,1705,1709,1717,1738,1764],{"__ignoreMap":155},[160,1560,1561],{"class":73,"line":162},[160,1562,1563],{"class":169},"\"\"\"admin.py\"\"\"\n",[160,1565,1566,1568,1571,1573],{"class":73,"line":225},[160,1567,327],{"class":218},[160,1569,1570],{"class":214}," django.contrib ",[160,1572,333],{"class":218},[160,1574,1575],{"class":214}," admin\n",[160,1577,1578,1580,1582,1584],{"class":73,"line":231},[160,1579,327],{"class":218},[160,1581,1078],{"class":214},[160,1583,333],{"class":218},[160,1585,1083],{"class":214},[160,1587,1588],{"class":73,"line":254},[160,1589,354],{"emptyLinePlaceholder":353},[160,1591,1592,1594,1596,1598,1600],{"class":73,"line":266},[160,1593,327],{"class":218},[160,1595,1094],{"class":214},[160,1597,333],{"class":218},[160,1599,1099],{"class":469},[160,1601,1102],{"class":214},[160,1603,1604],{"class":73,"line":298},[160,1605,354],{"emptyLinePlaceholder":353},[160,1607,1608,1611,1613,1616,1618,1621],{"class":73,"line":309},[160,1609,1610],{"class":578},"@admin.action",[160,1612,681],{"class":214},[160,1614,1615],{"class":237},"description",[160,1617,219],{"class":218},[160,1619,1620],{"class":169},"\"Export selected orders to Excel\"",[160,1622,263],{"class":214},[160,1624,1625,1627,1630],{"class":73,"line":483},[160,1626,575],{"class":218},[160,1628,1629],{"class":578}," export_selected",[160,1631,1632],{"class":214},"(modeladmin, request, queryset):\n",[160,1634,1635,1637,1639,1642,1644],{"class":73,"line":494},[160,1636,1147],{"class":214},[160,1638,219],{"class":218},[160,1640,1641],{"class":214}," queryset.select_related(",[160,1643,260],{"class":169},[160,1645,1646],{"class":214},").values(\n",[160,1648,1649,1652,1654,1656,1658,1660,1662,1664,1666,1668],{"class":73,"line":509},[160,1650,1651],{"class":169},"        \"reference\"",[160,1653,275],{"class":214},[160,1655,278],{"class":169},[160,1657,275],{"class":214},[160,1659,283],{"class":169},[160,1661,275],{"class":214},[160,1663,288],{"class":169},[160,1665,275],{"class":214},[160,1667,293],{"class":169},[160,1669,263],{"class":214},[160,1671,1672,1674,1676,1678,1680,1682,1684],{"class":73,"line":522},[160,1673,1208],{"class":214},[160,1675,219],{"class":218},[160,1677,1213],{"class":214},[160,1679,1216],{"class":237},[160,1681,219],{"class":218},[160,1683,470],{"class":469},[160,1685,263],{"class":214},[160,1687,1688,1690,1692,1694,1696],{"class":73,"line":535},[160,1689,1227],{"class":214},[160,1691,1230],{"class":169},[160,1693,697],{"class":214},[160,1695,219],{"class":218},[160,1697,1698],{"class":169}," 'attachment; filename=\"orders-selected.xlsx\"'\n",[160,1700,1701,1703],{"class":73,"line":548},[160,1702,1027],{"class":218},[160,1704,1244],{"class":214},[160,1706,1707],{"class":73,"line":561},[160,1708,354],{"emptyLinePlaceholder":353},[160,1710,1711,1714],{"class":73,"line":567},[160,1712,1713],{"class":578},"@admin.register",[160,1715,1716],{"class":214},"(Order)\n",[160,1718,1719,1722,1725,1727,1731,1733,1736],{"class":73,"line":572},[160,1720,1721],{"class":218},"class",[160,1723,1724],{"class":165}," OrderAdmin",[160,1726,681],{"class":214},[160,1728,1730],{"class":1729},"sHg5J","admin",[160,1732,18],{"class":214},[160,1734,1735],{"class":1729},"ModelAdmin",[160,1737,1502],{"class":214},[160,1739,1740,1743,1745,1748,1750,1752,1754,1756,1758,1760,1762],{"class":73,"line":602},[160,1741,1742],{"class":214},"    list_display ",[160,1744,219],{"class":218},[160,1746,1747],{"class":214}," (",[160,1749,272],{"class":169},[160,1751,275],{"class":214},[160,1753,260],{"class":169},[160,1755,275],{"class":214},[160,1757,288],{"class":169},[160,1759,275],{"class":214},[160,1761,293],{"class":169},[160,1763,263],{"class":214},[160,1765,1766,1769,1771],{"class":73,"line":619},[160,1767,1768],{"class":214},"    actions ",[160,1770,219],{"class":218},[160,1772,1773],{"class":214}," [export_selected]\n",[10,1775,1776,1777,1779],{},"An admin action returning an ",[157,1778,135],{}," sends it to the browser instead of redirecting, which is exactly the behaviour a download needs.",[145,1781,1783],{"id":1782},"escape-values-excel-would-run","Escape values Excel would run",[10,1785,1786,1787,275,1789,275,1791,1793,1794,1797],{},"Any text cell beginning ",[157,1788,219],{},[157,1790,965],{},[157,1792,381],{}," or ",[157,1795,1796],{},"@"," becomes a formula when the file opens, and in a Django app that text usually came from a user:",[150,1799,1801],{"className":205,"code":1800,"language":207,"meta":155,"style":155},"def escape_formula(value):\n    if isinstance(value, str) and value[:1] in (\"=\", \"+\", \"-\", \"@\"):\n        return \"'\" + value\n    return value\n",[157,1802,1803,1813,1861,1875],{"__ignoreMap":155},[160,1804,1805,1807,1810],{"class":73,"line":162},[160,1806,575],{"class":218},[160,1808,1809],{"class":578}," escape_formula",[160,1811,1812],{"class":214},"(value):\n",[160,1814,1815,1817,1820,1823,1825,1827,1830,1833,1835,1837,1839,1841,1844,1846,1849,1851,1854,1856,1859],{"class":73,"line":225},[160,1816,622],{"class":218},[160,1818,1819],{"class":469}," isinstance",[160,1821,1822],{"class":214},"(value, ",[160,1824,585],{"class":469},[160,1826,378],{"class":214},[160,1828,1829],{"class":218},"and",[160,1831,1832],{"class":214}," value[:",[160,1834,826],{"class":469},[160,1836,697],{"class":214},[160,1838,885],{"class":218},[160,1840,1747],{"class":214},[160,1842,1843],{"class":169},"\"=\"",[160,1845,275],{"class":214},[160,1847,1848],{"class":169},"\"+\"",[160,1850,275],{"class":214},[160,1852,1853],{"class":169},"\"-\"",[160,1855,275],{"class":214},[160,1857,1858],{"class":169},"\"@\"",[160,1860,1502],{"class":214},[160,1862,1863,1866,1869,1872],{"class":73,"line":231},[160,1864,1865],{"class":218},"        return",[160,1867,1868],{"class":169}," \"'\"",[160,1870,1871],{"class":218}," +",[160,1873,1874],{"class":214}," value\n",[160,1876,1877,1879],{"class":73,"line":254},[160,1878,1027],{"class":218},[160,1880,1874],{"class":214},[10,1882,1883,1884,1887],{},"Apply it as rows are written, or with ",[157,1885,1886],{},"df.map(escape_formula)"," before the write.",[145,1889,1891],{"id":1890},"test-the-view","Test the view",[150,1893,1895],{"className":205,"code":1894,"language":207,"meta":155,"style":155},"\"\"\"tests\u002Ftest_exports.py\"\"\"\nimport io\n\nimport pandas as pd\nfrom django.test import TestCase\nfrom django.urls import reverse\n\nclass ExportTests(TestCase):\n    def test_orders_export(self):\n        self.client.force_login(self.user)\n        resp = self.client.get(reverse(\"export-orders\"))\n\n        self.assertEqual(resp.status_code, 200)\n        self.assertIn(\"spreadsheetml.sheet\", resp[\"Content-Type\"])\n        self.assertIn(\"attachment\", resp[\"Content-Disposition\"])\n\n        df = pd.read_excel(io.BytesIO(resp.content), engine=\"openpyxl\")\n        self.assertEqual(list(df.columns)[:2], [\"Reference\", \"Created\"])\n\n    def test_export_requires_login(self):\n        resp = self.client.get(reverse(\"export-orders\"))\n        self.assertEqual(resp.status_code, 302)\n",[157,1896,1897,1902,1908,1912,1922,1934,1946,1950,1964,1975,1989,2007,2011,2022,2041,2056,2060,2078,2104,2108,2117,2131],{"__ignoreMap":155},[160,1898,1899],{"class":73,"line":162},[160,1900,1901],{"class":169},"\"\"\"tests\u002Ftest_exports.py\"\"\"\n",[160,1903,1904,1906],{"class":73,"line":225},[160,1905,333],{"class":218},[160,1907,443],{"class":214},[160,1909,1910],{"class":73,"line":231},[160,1911,354],{"emptyLinePlaceholder":353},[160,1913,1914,1916,1918,1920],{"class":73,"line":254},[160,1915,333],{"class":218},[160,1917,454],{"class":214},[160,1919,457],{"class":218},[160,1921,460],{"class":214},[160,1923,1924,1926,1929,1931],{"class":73,"line":266},[160,1925,327],{"class":218},[160,1927,1928],{"class":214}," django.test ",[160,1930,333],{"class":218},[160,1932,1933],{"class":214}," TestCase\n",[160,1935,1936,1938,1941,1943],{"class":73,"line":298},[160,1937,327],{"class":218},[160,1939,1940],{"class":214}," django.urls ",[160,1942,333],{"class":218},[160,1944,1945],{"class":214}," reverse\n",[160,1947,1948],{"class":73,"line":309},[160,1949,354],{"emptyLinePlaceholder":353},[160,1951,1952,1954,1957,1959,1962],{"class":73,"line":483},[160,1953,1721],{"class":218},[160,1955,1956],{"class":165}," ExportTests",[160,1958,681],{"class":214},[160,1960,1961],{"class":1729},"TestCase",[160,1963,1502],{"class":214},[160,1965,1966,1969,1972],{"class":73,"line":494},[160,1967,1968],{"class":218},"    def",[160,1970,1971],{"class":578}," test_orders_export",[160,1973,1974],{"class":214},"(self):\n",[160,1976,1977,1980,1983,1986],{"class":73,"line":509},[160,1978,1979],{"class":469},"        self",[160,1981,1982],{"class":214},".client.force_login(",[160,1984,1985],{"class":469},"self",[160,1987,1988],{"class":214},".user)\n",[160,1990,1991,1994,1996,1999,2002,2005],{"class":73,"line":522},[160,1992,1993],{"class":214},"        resp ",[160,1995,219],{"class":218},[160,1997,1998],{"class":469}," self",[160,2000,2001],{"class":214},".client.get(reverse(",[160,2003,2004],{"class":169},"\"export-orders\"",[160,2006,1199],{"class":214},[160,2008,2009],{"class":73,"line":535},[160,2010,354],{"emptyLinePlaceholder":353},[160,2012,2013,2015,2018,2020],{"class":73,"line":548},[160,2014,1979],{"class":469},[160,2016,2017],{"class":214},".assertEqual(resp.status_code, ",[160,2019,142],{"class":469},[160,2021,263],{"class":214},[160,2023,2024,2026,2029,2032,2035,2038],{"class":73,"line":561},[160,2025,1979],{"class":469},[160,2027,2028],{"class":214},".assertIn(",[160,2030,2031],{"class":169},"\"spreadsheetml.sheet\"",[160,2033,2034],{"class":214},", resp[",[160,2036,2037],{"class":169},"\"Content-Type\"",[160,2039,2040],{"class":214},"])\n",[160,2042,2043,2045,2047,2050,2052,2054],{"class":73,"line":567},[160,2044,1979],{"class":469},[160,2046,2028],{"class":214},[160,2048,2049],{"class":169},"\"attachment\"",[160,2051,2034],{"class":214},[160,2053,1230],{"class":169},[160,2055,2040],{"class":214},[160,2057,2058],{"class":73,"line":572},[160,2059,354],{"emptyLinePlaceholder":353},[160,2061,2062,2064,2066,2069,2071,2073,2076],{"class":73,"line":602},[160,2063,631],{"class":214},[160,2065,219],{"class":218},[160,2067,2068],{"class":214}," pd.read_excel(io.BytesIO(resp.content), ",[160,2070,740],{"class":237},[160,2072,219],{"class":218},[160,2074,2075],{"class":169},"\"openpyxl\"",[160,2077,263],{"class":214},[160,2079,2080,2082,2085,2087,2090,2093,2096,2098,2100,2102],{"class":73,"line":619},[160,2081,1979],{"class":469},[160,2083,2084],{"class":214},".assertEqual(",[160,2086,613],{"class":469},[160,2088,2089],{"class":214},"(df.columns)[:",[160,2091,2092],{"class":469},"2",[160,2094,2095],{"class":214},"], [",[160,2097,503],{"class":169},[160,2099,275],{"class":214},[160,2101,517],{"class":169},[160,2103,2040],{"class":214},[160,2105,2106],{"class":73,"line":628},[160,2107,354],{"emptyLinePlaceholder":353},[160,2109,2110,2112,2115],{"class":73,"line":651},[160,2111,1968],{"class":218},[160,2113,2114],{"class":578}," test_export_requires_login",[160,2116,1974],{"class":214},[160,2118,2119,2121,2123,2125,2127,2129],{"class":73,"line":659},[160,2120,1993],{"class":214},[160,2122,219],{"class":218},[160,2124,1998],{"class":469},[160,2126,2001],{"class":214},[160,2128,2004],{"class":169},[160,2130,1199],{"class":214},[160,2132,2133,2135,2137,2140],{"class":73,"line":689},[160,2134,1979],{"class":469},[160,2136,2017],{"class":214},[160,2138,2139],{"class":469},"302",[160,2141,263],{"class":214},[10,2143,2144],{},"The second test matters as much as the first: an export view that quietly works while logged out is a data leak, and it is the check most easily forgotten.",[145,2146,2148],{"id":2147},"let-the-user-choose-the-columns-safely","Let the user choose the columns, safely",[10,2150,2151],{},"Export screens usually offer a column picker, and that is a place where user input reaches the ORM. Validate the selection against an allow-list so a crafted parameter cannot pull a password hash or traverse a relation you did not intend:",[150,2153,2155],{"className":205,"code":2154,"language":207,"meta":155,"style":155},"\"\"\"Only these paths may ever appear in an export.\"\"\"\nEXPORTABLE = {\n    \"reference\": \"Reference\",\n    \"created\": \"Created\",\n    \"customer__name\": \"Customer\",\n    \"region\": \"Region\",\n    \"total\": \"Total\",\n    \"status\": \"Status\",\n}\n\ndef chosen_columns(request) -> dict[str, str]:\n    wanted = request.GET.getlist(\"columns\") or list(EXPORTABLE)\n    picked = {key: EXPORTABLE[key] for key in wanted if key in EXPORTABLE}\n    return picked or dict(list(EXPORTABLE.items())[:3])\n",[157,2156,2157,2162,2171,2181,2191,2201,2211,2221,2233,2237,2241,2260,2293,2328],{"__ignoreMap":155},[160,2158,2159],{"class":73,"line":162},[160,2160,2161],{"class":169},"\"\"\"Only these paths may ever appear in an export.\"\"\"\n",[160,2163,2164,2167,2169],{"class":73,"line":225},[160,2165,2166],{"class":469},"EXPORTABLE",[160,2168,473],{"class":218},[160,2170,491],{"class":214},[160,2172,2173,2175,2177,2179],{"class":73,"line":231},[160,2174,497],{"class":169},[160,2176,500],{"class":214},[160,2178,503],{"class":169},[160,2180,506],{"class":214},[160,2182,2183,2185,2187,2189],{"class":73,"line":254},[160,2184,512],{"class":169},[160,2186,500],{"class":214},[160,2188,517],{"class":169},[160,2190,506],{"class":214},[160,2192,2193,2195,2197,2199],{"class":73,"line":266},[160,2194,525],{"class":169},[160,2196,500],{"class":214},[160,2198,530],{"class":169},[160,2200,506],{"class":214},[160,2202,2203,2205,2207,2209],{"class":73,"line":298},[160,2204,538],{"class":169},[160,2206,500],{"class":214},[160,2208,543],{"class":169},[160,2210,506],{"class":214},[160,2212,2213,2215,2217,2219],{"class":73,"line":309},[160,2214,551],{"class":169},[160,2216,500],{"class":214},[160,2218,556],{"class":169},[160,2220,506],{"class":214},[160,2222,2223,2226,2228,2231],{"class":73,"line":483},[160,2224,2225],{"class":169},"    \"status\"",[160,2227,500],{"class":214},[160,2229,2230],{"class":169},"\"Status\"",[160,2232,506],{"class":214},[160,2234,2235],{"class":73,"line":494},[160,2236,564],{"class":214},[160,2238,2239],{"class":73,"line":509},[160,2240,354],{"emptyLinePlaceholder":353},[160,2242,2243,2245,2248,2251,2253,2255,2257],{"class":73,"line":522},[160,2244,575],{"class":218},[160,2246,2247],{"class":578}," chosen_columns",[160,2249,2250],{"class":214},"(request) -> dict[",[160,2252,585],{"class":469},[160,2254,275],{"class":214},[160,2256,585],{"class":469},[160,2258,2259],{"class":214},"]:\n",[160,2261,2262,2265,2267,2270,2273,2276,2279,2281,2284,2287,2289,2291],{"class":73,"line":535},[160,2263,2264],{"class":214},"    wanted ",[160,2266,219],{"class":218},[160,2268,2269],{"class":214}," request.",[160,2271,2272],{"class":469},"GET",[160,2274,2275],{"class":214},".getlist(",[160,2277,2278],{"class":169},"\"columns\"",[160,2280,378],{"class":214},[160,2282,2283],{"class":218},"or",[160,2285,2286],{"class":469}," list",[160,2288,681],{"class":214},[160,2290,2166],{"class":469},[160,2292,263],{"class":214},[160,2294,2295,2298,2300,2303,2305,2308,2310,2312,2314,2317,2319,2321,2323,2326],{"class":73,"line":548},[160,2296,2297],{"class":214},"    picked ",[160,2299,219],{"class":218},[160,2301,2302],{"class":214}," {key: ",[160,2304,2166],{"class":469},[160,2306,2307],{"class":214},"[key] ",[160,2309,1515],{"class":218},[160,2311,1518],{"class":214},[160,2313,885],{"class":218},[160,2315,2316],{"class":214}," wanted ",[160,2318,916],{"class":218},[160,2320,1518],{"class":214},[160,2322,885],{"class":218},[160,2324,2325],{"class":469}," EXPORTABLE",[160,2327,564],{"class":214},[160,2329,2330,2332,2335,2337,2340,2342,2344,2346,2348,2351,2354],{"class":73,"line":561},[160,2331,1027],{"class":218},[160,2333,2334],{"class":214}," picked ",[160,2336,2283],{"class":218},[160,2338,2339],{"class":469}," dict",[160,2341,681],{"class":214},[160,2343,613],{"class":469},[160,2345,681],{"class":214},[160,2347,2166],{"class":469},[160,2349,2350],{"class":214},".items())[:",[160,2352,2353],{"class":469},"3",[160,2355,2040],{"class":214},[10,2357,2358,2359,2361],{},"Filtering against the dictionary rather than trusting the list is the whole safeguard: an unknown key is dropped rather than passed to ",[157,2360,201],{},", and the fallback guarantees the export is never empty of columns. The same principle applies to sort keys and filter fields — an allow-list, not a denial-list.",[20,2363,29,2368,29,2371,29,2374,29,2379,29,2382,29,2387,29,2392,29,2395,29,2399,29,2403,29,2406,29,2409,29,2411,29,2414,29,2417,29,2421,29,2424,29,2427,29,2431,29,2434],{"viewBox":2364,"role":23,"ariaLabelledBy":2365,"xmlns":27,"style":28},"14 -3 732 171",[2366,2367],"dj3-t","dj3-d",[31,2369,2370],{"id":2366},"Column selection filtered through an allow-list",[35,2372,2373],{"id":2367},"Requested column names are intersected with a fixed set of exportable paths, so unknown or crafted names are dropped before they reach the queryset.",[39,2375],{"x":973,"y":2376,"width":2377,"height":2378,"fill":44},"-3","732","171",[46,2380,2381],{"x":48,"y":49,"style":50},"Requested names never reach values() directly",[39,2383],{"x":1280,"y":2384,"width":2385,"height":64,"rx":58,"fill":2386,"stroke":60},"48","190","#fee8f2",[46,2388,2391],{"x":2389,"y":1287,"style":2390},"125","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","requested",[46,2393,2394],{"x":2389,"y":64,"style":1292},"region, total,",[46,2396,2398],{"x":2389,"y":2397,"style":1292},"128","owner__password",[73,2400],{"x1":77,"y1":2401,"x2":2402,"y2":2401,"stroke":78,"style":79},"100","264",[81,2404],{"points":2405,"fill":84},"264,100 254,95 254,105",[39,2407],{"x":2408,"y":2384,"width":87,"height":64,"rx":58,"fill":88,"stroke":60},"268",[46,2410,2166],{"x":48,"y":1287,"style":92},[46,2412,2413],{"x":48,"y":64,"style":1292},"reference, created, customer,",[46,2415,2416],{"x":48,"y":2397,"style":1292},"region, total, status",[73,2418],{"x1":2419,"y1":2401,"x2":2420,"y2":2401,"stroke":121,"style":79},"492","536",[81,2422],{"points":2423,"fill":125},"536,100 526,95 526,105",[39,2425],{"x":2426,"y":2384,"width":2385,"height":64,"rx":58,"fill":130,"stroke":60},"540",[46,2428,2430],{"x":2429,"y":1287,"style":134},"635","exported",[46,2432,2433],{"x":2429,"y":64,"style":138},"region, total",[46,2435,2436],{"x":2429,"y":2397,"style":70},"unknown key dropped",[145,2438,2440],{"id":2439},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2442,2443,2444,2456,2465,2471,2487],"ul",{},[2445,2446,2447,2451,2452,2455],"li",{},[2448,2449,2450],"strong",{},"Iterating model instances."," ",[157,2453,2454],{},"for order in qs:"," builds an object per row to read four fields from it.",[2445,2457,2458,2464],{},[2448,2459,2460,2461,18],{},"Missing ",[157,2462,2463],{},"select_related"," A related column without it issues one query per row.",[2445,2466,2467,2470],{},[2448,2468,2469],{},"Timezone-aware datetimes."," Strip or convert them before writing; Excel cannot store an offset.",[2445,2472,2473,2479,2480,2482,2483,2486],{},[2448,2474,2475,2478],{},[157,2476,2477],{},"Decimal"," columns."," They arrive as ",[157,2481,2477],{}," and write as text in some paths; cast to ",[157,2484,2485],{},"float"," for numeric cells.",[2445,2488,2489,2492,2493,2495],{},[2448,2490,2491],{},"Exporting the whole table by accident."," An unfiltered queryset in an admin action ignores the selection if you rebuild it from the model rather than using the ",[157,2494,66],{}," argument.",[145,2497,2499],{"id":2498},"performance-and-scale-notes","Performance and scale notes",[10,2501,2502,2503,2505,2506,2508,2509,2512,2513,18],{},"The database side dominates for wide exports: ",[157,2504,201],{}," plus ",[157,2507,2463],{}," turns an N+1 pattern into one query, and ",[157,2510,2511],{},"iterator(chunk_size=...)"," keeps the result set from being materialised at once. The workbook side is bounded by memory, roughly the finished file plus the rows in flight. Between them, a hundred thousand rows is comfortable inline on a normal instance and a million is not — at that size, queue the build, store the result, and send a link. If the recipient is a system rather than a person, a streamed CSV response has neither limit; the comparison is in ",[14,2514,2516],{"href":2515},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python\u002F","Convert Excel to CSV with Python",[145,2518,2520],{"id":2519},"conclusion","Conclusion",[10,2522,2523,2524,2526,2527,2529],{},"Export from Django by asking the database for columns, not objects: ",[157,2525,201],{}," with ",[157,2528,2463],{},", annotated where a value needs computing, filtered to what the caller may see. Hand those rows to a builder that returns bytes, and return them with the spreadsheet content type and an attachment disposition. Reuse the same builder for an admin action and a background job, strip timezones before writing, escape leading formula characters, and test that the view refuses anonymous requests.",[145,2531,2533],{"id":2532},"frequently-asked-questions","Frequently asked questions",[10,2535,2536,2539,2541],{},[2448,2537,2538],{},"Should I use values() or iterate model instances?",[157,2540,201],{}," for exports. It returns dictionaries straight from the database without building model objects, which is both faster and lighter — and an export needs columns, not behaviour.",[10,2543,2544,2547,2548,2550,2551,2554,2555,2557],{},[2448,2545,2546],{},"How do I include a related object's field?","\nUse double-underscore paths in ",[157,2549,201],{},", for example ",[157,2552,2553],{},"customer__name",", and add ",[157,2556,2463],{}," for the same relations so the query stays a single join rather than one query per row.",[10,2559,2560,2563,2564,2566],{},[2448,2561,2562],{},"What about a queryset with a million rows?","\nDo not serve it inline. Use ",[157,2565,2511],{}," to keep memory flat, and move the whole job to a background task that stores the file, because the request will time out long before the workbook is ready.",[10,2568,2569,2572,2573,2575],{},[2448,2570,2571],{},"Can I add this to the Django admin?","\nYes. An admin action receives the selected queryset and can return an ",[157,2574,135],{},", so the same builder function serves both a view and an admin bulk export.",[10,2577,2578,2581],{},[2448,2579,2580],{},"Do I need pandas?","\nNo. xlsxwriter can write rows directly from the queryset, which avoids materialising a DataFrame. pandas is convenient when you also want to reshape or aggregate before exporting.",[145,2583,2585],{"id":2584},"related","Related",[2442,2587,2588,2596,2603,2610,2617],{},[2445,2589,2590,2451,2593,2595],{},[2448,2591,2592],{},"Up:",[14,2594,17],{"href":16}," — headers, caching, background jobs and the other frameworks.",[2445,2597,2598,2602],{},[14,2599,2601],{"href":2600},"\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 buffer rules the builder above relies on.",[2445,2604,2605,2609],{},[14,2606,2608],{"href":2607},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Freturn-an-excel-file-from-a-flask-download-endpoint\u002F","Return an Excel file from a Flask download endpoint"," — the same view in a smaller framework.",[2445,2611,2612,2616],{},[14,2613,2615],{"href":2614},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-sql-query-results-to-excel-with-python\u002F","Export SQL query results to Excel with Python"," — the same job without an ORM in the way.",[2445,2618,2619,2623],{},[14,2620,2622],{"href":2621},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-formatted-excel-report-with-xlsxwriter\u002F","Write a formatted Excel report with xlsxwriter"," — going further with the formatting applied above.",[2625,2626,2627],"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 .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}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 .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}",{"title":155,"searchDepth":225,"depth":225,"links":2629},[2630,2631,2632,2633,2634,2635,2636,2637,2638,2639,2640,2641,2642,2643],{"id":147,"depth":225,"text":148},{"id":195,"depth":225,"text":196},{"id":422,"depth":225,"text":423},{"id":1048,"depth":225,"text":1049},{"id":1319,"depth":225,"text":1320},{"id":1549,"depth":225,"text":1550},{"id":1782,"depth":225,"text":1783},{"id":1890,"depth":225,"text":1891},{"id":2147,"depth":225,"text":2148},{"id":2439,"depth":225,"text":2440},{"id":2498,"depth":225,"text":2499},{"id":2519,"depth":225,"text":2520},{"id":2532,"depth":225,"text":2533},{"id":2584,"depth":225,"text":2585},"2026-08-27","Turn a Django queryset into a formatted .xlsx download: values() and related fields, iterator() for large exports, permissions, admin actions and a view test.","md",[2648,2650,2652,2654,2656],{"q":2538,"a":2649},"values() for exports. It returns dictionaries straight from the database without building model objects, which is both faster and lighter — and an export needs columns, not behaviour.",{"q":2546,"a":2651},"Use double-underscore paths in values(), for example customer__name, and add select_related for the same relations so the query stays a single join rather than one query per row.",{"q":2562,"a":2653},"Do not serve it inline. Use iterator(chunk_size=...) to keep memory flat, and move the whole job to a background task that stores the file, because the request will time out long before the workbook is ready.",{"q":2571,"a":2655},"Yes. An admin action receives the selected queryset and can return an HttpResponse, so the same builder function serves both a view and an admin bulk export.",{"q":2580,"a":2657},"No. xlsxwriter can write rows directly from the queryset, which avoids materialising a DataFrame. pandas is convenient when you also want to reshape or aggregate before exporting.",{},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel",{"title":2661,"description":2662},"Django Queryset to Excel Export","Export Django querysets to xlsx: values() with related fields, verbose column headers, iterator() and chunking, an admin action, permissions and a response test.","export-a-django-queryset-to-excel","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002Findex","how-to","C6esesFf6msGGmZBAu0fki2xeYLczZk4zU_7lj5mnBE",[2668,2672],{"title":2669,"path":2670,"stem":2671,"children":-1},"Build an Excel Workbook in Memory with BytesIO","\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002Findex",{"title":2673,"path":2674,"stem":2675,"children":-1},"Handle an Uploaded Excel File in Flask and FastAPI","\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fhandle-an-uploaded-excel-file-in-flask-and-fastapi\u002Findex",1788710158880]