Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/templatetags/admin_list.py: 15%

224 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import datetime 

2 

3from django.conf import settings 

4from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 

5from django.contrib.admin.utils import ( 

6 display_for_field, 

7 display_for_value, 

8 get_fields_from_path, 

9 label_for_field, 

10 lookup_field, 

11) 

12from django.contrib.admin.views.main import ( 

13 ALL_VAR, 

14 IS_POPUP_VAR, 

15 ORDER_VAR, 

16 PAGE_VAR, 

17 SEARCH_VAR, 

18) 

19from django.core.exceptions import ObjectDoesNotExist 

20from django.db import models 

21from django.template import Library 

22from django.template.loader import get_template 

23from django.templatetags.static import static 

24from django.urls import NoReverseMatch 

25from django.utils import formats, timezone 

26from django.utils.html import format_html 

27from django.utils.safestring import mark_safe 

28from django.utils.text import capfirst 

29from django.utils.translation import gettext as _ 

30 

31from .base import InclusionAdminNode 

32 

33register = Library() 

34 

35 

36@register.simple_tag 

37def paginator_number(cl, i): 

38 """ 

39 Generate an individual page index link in a paginated list. 

40 """ 

41 if i == cl.paginator.ELLIPSIS: 

42 return format_html("{} ", cl.paginator.ELLIPSIS) 

43 elif i == cl.page_num: 

44 return format_html('<span class="this-page">{}</span> ', i) 

45 else: 

46 return format_html( 

47 '<a href="{}"{}>{}</a> ', 

48 cl.get_query_string({PAGE_VAR: i}), 

49 mark_safe(' class="end"' if i == cl.paginator.num_pages else ""), 

50 i, 

51 ) 

52 

53 

54def pagination(cl): 

55 """ 

56 Generate the series of links to the pages in a paginated list. 

57 """ 

58 pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page 

59 page_range = ( 

60 cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else [] 

61 ) 

62 need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page 

63 return { 

64 "cl": cl, 

65 "pagination_required": pagination_required, 

66 "show_all_url": need_show_all_link and cl.get_query_string({ALL_VAR: ""}), 

67 "page_range": page_range, 

68 "ALL_VAR": ALL_VAR, 

69 "1": 1, 

70 } 

71 

72 

73@register.tag(name="pagination") 

74def pagination_tag(parser, token): 

75 return InclusionAdminNode( 

76 parser, 

77 token, 

78 func=pagination, 

79 template_name="pagination.html", 

80 takes_context=False, 

81 ) 

82 

83 

84def result_headers(cl): 

85 """ 

86 Generate the list column headers. 

87 """ 

88 ordering_field_columns = cl.get_ordering_field_columns() 

89 for i, field_name in enumerate(cl.list_display): 

90 text, attr = label_for_field( 

91 field_name, cl.model, model_admin=cl.model_admin, return_attr=True 

92 ) 

93 is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by 

94 if attr: 

95 field_name = _coerce_field_name(field_name, i) 

96 # Potentially not sortable 

97 

98 # if the field is the action checkbox: no sorting and special class 

99 if field_name == "action_checkbox": 

100 yield { 

101 "text": text, 

102 "class_attrib": mark_safe(' class="action-checkbox-column"'), 

103 "sortable": False, 

104 } 

105 continue 

106 

107 admin_order_field = getattr(attr, "admin_order_field", None) 

108 # Set ordering for attr that is a property, if defined. 

109 if isinstance(attr, property) and hasattr(attr, "fget"): 

110 admin_order_field = getattr(attr.fget, "admin_order_field", None) 

111 if not admin_order_field: 

112 is_field_sortable = False 

113 

114 if not is_field_sortable: 

115 # Not sortable 

116 yield { 

117 "text": text, 

118 "class_attrib": format_html(' class="column-{}"', field_name), 

119 "sortable": False, 

120 } 

121 continue 

122 

123 # OK, it is sortable if we got this far 

124 th_classes = ["sortable", "column-{}".format(field_name)] 

125 order_type = "" 

126 new_order_type = "asc" 

127 sort_priority = 0 

128 # Is it currently being sorted on? 

129 is_sorted = i in ordering_field_columns 

130 if is_sorted: 

131 order_type = ordering_field_columns.get(i).lower() 

132 sort_priority = list(ordering_field_columns).index(i) + 1 

133 th_classes.append("sorted %sending" % order_type) 

134 new_order_type = {"asc": "desc", "desc": "asc"}[order_type] 

135 

136 # build new ordering param 

137 o_list_primary = [] # URL for making this field the primary sort 

138 o_list_remove = [] # URL for removing this field from sort 

139 o_list_toggle = [] # URL for toggling order type for this field 

140 

141 def make_qs_param(t, n): 

142 return ("-" if t == "desc" else "") + str(n) 

143 

144 for j, ot in ordering_field_columns.items(): 

145 if j == i: # Same column 

146 param = make_qs_param(new_order_type, j) 

147 # We want clicking on this header to bring the ordering to the 

148 # front 

149 o_list_primary.insert(0, param) 

150 o_list_toggle.append(param) 

151 # o_list_remove - omit 

152 else: 

153 param = make_qs_param(ot, j) 

154 o_list_primary.append(param) 

155 o_list_toggle.append(param) 

156 o_list_remove.append(param) 

157 

158 if i not in ordering_field_columns: 

159 o_list_primary.insert(0, make_qs_param(new_order_type, i)) 

160 

161 yield { 

162 "text": text, 

163 "sortable": True, 

164 "sorted": is_sorted, 

165 "ascending": order_type == "asc", 

166 "sort_priority": sort_priority, 

167 "url_primary": cl.get_query_string({ORDER_VAR: ".".join(o_list_primary)}), 

168 "url_remove": cl.get_query_string({ORDER_VAR: ".".join(o_list_remove)}), 

169 "url_toggle": cl.get_query_string({ORDER_VAR: ".".join(o_list_toggle)}), 

170 "class_attrib": format_html(' class="{}"', " ".join(th_classes)) 

171 if th_classes 

172 else "", 

173 } 

174 

175 

176def _boolean_icon(field_val): 

177 icon_url = static( 

178 "admin/img/icon-%s.svg" % {True: "yes", False: "no", None: "unknown"}[field_val] 

179 ) 

180 return format_html('<img src="{}" alt="{}">', icon_url, field_val) 

181 

182 

183def _coerce_field_name(field_name, field_index): 

184 """ 

185 Coerce a field_name (which may be a callable) to a string. 

186 """ 

187 if callable(field_name): 

188 if field_name.__name__ == "<lambda>": 

189 return "lambda" + str(field_index) 

190 else: 

191 return field_name.__name__ 

192 return field_name 

193 

194 

195def items_for_result(cl, result, form): 

196 """ 

197 Generate the actual list of data. 

198 """ 

199 

200 def link_in_col(is_first, field_name, cl): 

201 if cl.list_display_links is None: 

202 return False 

203 if is_first and not cl.list_display_links: 

204 return True 

205 return field_name in cl.list_display_links 

206 

207 first = True 

208 pk = cl.lookup_opts.pk.attname 

209 for field_index, field_name in enumerate(cl.list_display): 

210 empty_value_display = cl.model_admin.get_empty_value_display() 

211 row_classes = ["field-%s" % _coerce_field_name(field_name, field_index)] 

212 try: 

213 f, attr, value = lookup_field(field_name, result, cl.model_admin) 

214 except ObjectDoesNotExist: 

215 result_repr = empty_value_display 

216 else: 

217 empty_value_display = getattr( 

218 attr, "empty_value_display", empty_value_display 

219 ) 

220 if f is None or f.auto_created: 

221 if field_name == "action_checkbox": 

222 row_classes = ["action-checkbox"] 

223 boolean = getattr(attr, "boolean", False) 

224 result_repr = display_for_value(value, empty_value_display, boolean) 

225 if isinstance(value, (datetime.date, datetime.time)): 

226 row_classes.append("nowrap") 

227 else: 

228 if isinstance(f.remote_field, models.ManyToOneRel): 

229 field_val = getattr(result, f.name) 

230 if field_val is None: 

231 result_repr = empty_value_display 

232 else: 

233 result_repr = field_val 

234 else: 

235 result_repr = display_for_field(value, f, empty_value_display) 

236 if isinstance( 

237 f, (models.DateField, models.TimeField, models.ForeignKey) 

238 ): 

239 row_classes.append("nowrap") 

240 row_class = mark_safe(' class="%s"' % " ".join(row_classes)) 

241 # If list_display_links not defined, add the link tag to the first field 

242 if link_in_col(first, field_name, cl): 

243 table_tag = "th" if first else "td" 

244 first = False 

245 

246 # Display link to the result's change_view if the url exists, else 

247 # display just the result's representation. 

248 try: 

249 url = cl.url_for_result(result) 

250 except NoReverseMatch: 

251 link_or_text = result_repr 

252 else: 

253 url = add_preserved_filters( 

254 {"preserved_filters": cl.preserved_filters, "opts": cl.opts}, url 

255 ) 

256 # Convert the pk to something that can be used in JavaScript. 

257 # Problem cases are non-ASCII strings. 

258 if cl.to_field: 

259 attr = str(cl.to_field) 

260 else: 

261 attr = pk 

262 value = result.serializable_value(attr) 

263 link_or_text = format_html( 

264 '<a href="{}"{}>{}</a>', 

265 url, 

266 format_html(' data-popup-opener="{}"', value) 

267 if cl.is_popup 

268 else "", 

269 result_repr, 

270 ) 

271 

272 yield format_html( 

273 "<{}{}>{}</{}>", table_tag, row_class, link_or_text, table_tag 

274 ) 

275 else: 

276 # By default the fields come from ModelAdmin.list_editable, but if we pull 

277 # the fields out of the form instead of list_editable custom admins 

278 # can provide fields on a per request basis 

279 if ( 

280 form 

281 and field_name in form.fields 

282 and not ( 

283 field_name == cl.model._meta.pk.name 

284 and form[cl.model._meta.pk.name].is_hidden 

285 ) 

286 ): 

287 bf = form[field_name] 

288 result_repr = mark_safe(str(bf.errors) + str(bf)) 

289 yield format_html("<td{}>{}</td>", row_class, result_repr) 

290 if form and not form[cl.model._meta.pk.name].is_hidden: 

291 yield format_html("<td>{}</td>", form[cl.model._meta.pk.name]) 

292 

293 

294class ResultList(list): 

295 """ 

296 Wrapper class used to return items in a list_editable changelist, annotated 

297 with the form object for error reporting purposes. Needed to maintain 

298 backwards compatibility with existing admin templates. 

299 """ 

300 

301 def __init__(self, form, *items): 

302 self.form = form 

303 super().__init__(*items) 

304 

305 

306def results(cl): 

307 if cl.formset: 

308 for res, form in zip(cl.result_list, cl.formset.forms): 

309 yield ResultList(form, items_for_result(cl, res, form)) 

310 else: 

311 for res in cl.result_list: 

312 yield ResultList(None, items_for_result(cl, res, None)) 

313 

314 

315def result_hidden_fields(cl): 

316 if cl.formset: 

317 for res, form in zip(cl.result_list, cl.formset.forms): 

318 if form[cl.model._meta.pk.name].is_hidden: 

319 yield mark_safe(form[cl.model._meta.pk.name]) 

320 

321 

322def result_list(cl): 

323 """ 

324 Display the headers and data list together. 

325 """ 

326 headers = list(result_headers(cl)) 

327 num_sorted_fields = 0 

328 for h in headers: 

329 if h["sortable"] and h["sorted"]: 

330 num_sorted_fields += 1 

331 return { 

332 "cl": cl, 

333 "result_hidden_fields": list(result_hidden_fields(cl)), 

334 "result_headers": headers, 

335 "num_sorted_fields": num_sorted_fields, 

336 "results": list(results(cl)), 

337 } 

338 

339 

340@register.tag(name="result_list") 

341def result_list_tag(parser, token): 

342 return InclusionAdminNode( 

343 parser, 

344 token, 

345 func=result_list, 

346 template_name="change_list_results.html", 

347 takes_context=False, 

348 ) 

349 

350 

351def date_hierarchy(cl): 

352 """ 

353 Display the date hierarchy for date drill-down functionality. 

354 """ 

355 if cl.date_hierarchy: 

356 field_name = cl.date_hierarchy 

357 field = get_fields_from_path(cl.model, field_name)[-1] 

358 if isinstance(field, models.DateTimeField): 

359 dates_or_datetimes = "datetimes" 

360 qs_kwargs = {"is_dst": True} if settings.USE_DEPRECATED_PYTZ else {} 

361 else: 

362 dates_or_datetimes = "dates" 

363 qs_kwargs = {} 

364 year_field = "%s__year" % field_name 

365 month_field = "%s__month" % field_name 

366 day_field = "%s__day" % field_name 

367 field_generic = "%s__" % field_name 

368 year_lookup = cl.params.get(year_field) 

369 month_lookup = cl.params.get(month_field) 

370 day_lookup = cl.params.get(day_field) 

371 

372 def link(filters): 

373 return cl.get_query_string(filters, [field_generic]) 

374 

375 if not (year_lookup or month_lookup or day_lookup): 

376 # select appropriate start level 

377 date_range = cl.queryset.aggregate( 

378 first=models.Min(field_name), last=models.Max(field_name) 

379 ) 

380 if date_range["first"] and date_range["last"]: 

381 if dates_or_datetimes == "datetimes": 

382 date_range = { 

383 k: timezone.localtime(v) if timezone.is_aware(v) else v 

384 for k, v in date_range.items() 

385 } 

386 if date_range["first"].year == date_range["last"].year: 

387 year_lookup = date_range["first"].year 

388 if date_range["first"].month == date_range["last"].month: 

389 month_lookup = date_range["first"].month 

390 

391 if year_lookup and month_lookup and day_lookup: 

392 day = datetime.date(int(year_lookup), int(month_lookup), int(day_lookup)) 

393 return { 

394 "show": True, 

395 "back": { 

396 "link": link({year_field: year_lookup, month_field: month_lookup}), 

397 "title": capfirst(formats.date_format(day, "YEAR_MONTH_FORMAT")), 

398 }, 

399 "choices": [ 

400 {"title": capfirst(formats.date_format(day, "MONTH_DAY_FORMAT"))} 

401 ], 

402 } 

403 elif year_lookup and month_lookup: 

404 days = getattr(cl.queryset, dates_or_datetimes)( 

405 field_name, "day", **qs_kwargs 

406 ) 

407 return { 

408 "show": True, 

409 "back": { 

410 "link": link({year_field: year_lookup}), 

411 "title": str(year_lookup), 

412 }, 

413 "choices": [ 

414 { 

415 "link": link( 

416 { 

417 year_field: year_lookup, 

418 month_field: month_lookup, 

419 day_field: day.day, 

420 } 

421 ), 

422 "title": capfirst(formats.date_format(day, "MONTH_DAY_FORMAT")), 

423 } 

424 for day in days 

425 ], 

426 } 

427 elif year_lookup: 

428 months = getattr(cl.queryset, dates_or_datetimes)( 

429 field_name, "month", **qs_kwargs 

430 ) 

431 return { 

432 "show": True, 

433 "back": {"link": link({}), "title": _("All dates")}, 

434 "choices": [ 

435 { 

436 "link": link( 

437 {year_field: year_lookup, month_field: month.month} 

438 ), 

439 "title": capfirst( 

440 formats.date_format(month, "YEAR_MONTH_FORMAT") 

441 ), 

442 } 

443 for month in months 

444 ], 

445 } 

446 else: 

447 years = getattr(cl.queryset, dates_or_datetimes)( 

448 field_name, "year", **qs_kwargs 

449 ) 

450 return { 

451 "show": True, 

452 "back": None, 

453 "choices": [ 

454 { 

455 "link": link({year_field: str(year.year)}), 

456 "title": str(year.year), 

457 } 

458 for year in years 

459 ], 

460 } 

461 

462 

463@register.tag(name="date_hierarchy") 

464def date_hierarchy_tag(parser, token): 

465 return InclusionAdminNode( 

466 parser, 

467 token, 

468 func=date_hierarchy, 

469 template_name="date_hierarchy.html", 

470 takes_context=False, 

471 ) 

472 

473 

474def search_form(cl): 

475 """ 

476 Display a search form for searching the list. 

477 """ 

478 return { 

479 "cl": cl, 

480 "show_result_count": cl.result_count != cl.full_result_count, 

481 "search_var": SEARCH_VAR, 

482 "is_popup_var": IS_POPUP_VAR, 

483 } 

484 

485 

486@register.tag(name="search_form") 

487def search_form_tag(parser, token): 

488 return InclusionAdminNode( 

489 parser, 

490 token, 

491 func=search_form, 

492 template_name="search_form.html", 

493 takes_context=False, 

494 ) 

495 

496 

497@register.simple_tag 

498def admin_list_filter(cl, spec): 

499 tpl = get_template(spec.template) 

500 return tpl.render( 

501 { 

502 "title": spec.title, 

503 "choices": list(spec.choices(cl)), 

504 "spec": spec, 

505 } 

506 ) 

507 

508 

509def admin_actions(context): 

510 """ 

511 Track the number of times the action field has been rendered on the page, 

512 so we know which value to use. 

513 """ 

514 context["action_index"] = context.get("action_index", -1) + 1 

515 return context 

516 

517 

518@register.tag(name="admin_actions") 

519def admin_actions_tag(parser, token): 

520 return InclusionAdminNode( 

521 parser, token, func=admin_actions, template_name="actions.html" 

522 ) 

523 

524 

525@register.tag(name="change_list_object_tools") 

526def change_list_object_tools_tag(parser, token): 

527 """Display the row of change list object tools.""" 

528 return InclusionAdminNode( 

529 parser, 

530 token, 

531 func=lambda context: context, 

532 template_name="change_list_object_tools.html", 

533 )