Coverage for src/django_resume/plugins.py: 98%

433 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-27 17:54 +0200

1from uuid import uuid4 

2 

3from typing import Protocol, runtime_checkable, Callable, TypeAlias, Any 

4 

5from django import forms 

6from django.http import HttpResponse 

7from django.shortcuts import get_object_or_404, render 

8from django.urls import reverse, path, URLPattern, URLResolver 

9from django.utils.html import format_html 

10 

11from .models import Person 

12 

13 

14URLPatterns: TypeAlias = list[URLPattern | URLResolver] 

15FormClasses: TypeAlias = dict[str, type[forms.Form]] 

16 

17 

18@runtime_checkable 

19class Plugin(Protocol): 

20 name: str 

21 verbose_name: str 

22 form_classes: FormClasses 

23 

24 def get_admin_urls(self, admin_view: Callable) -> URLPatterns: 

25 """Return a list of urls that are used to manage the plugin data in the Django admin interface.""" 

26 ... # pragma: no cover 

27 

28 def get_admin_link(self, person_id: int) -> str: 

29 """Return a formatted html link to the main admin view for this plugin.""" 

30 ... # pragma: no cover 

31 

32 def get_inline_urls(self) -> URLPatterns: 

33 """Return a list of urls that are used to manage the plugin data inline.""" 

34 ... # pragma: no cover 

35 

36 def get_form_classes(self) -> FormClasses: 

37 """ 

38 Return a dictionary of form classes that are used to manage the plugin data. 

39 Overwrite this method or set the form_classes attribute. 

40 """ 

41 ... # pragma: no cover 

42 

43 def get_data(self, person: Person) -> dict: 

44 """Return the plugin data for a person.""" 

45 ... # pragma: no cover 

46 

47 def get_context( 

48 self, plugin_data: dict, person_pk: int, *, context: dict 

49 ) -> object: 

50 """Return the object which is stored in context for the plugin.""" 

51 ... # pragma: no cover 

52 

53 

54class SimpleData: 

55 def __init__(self, *, plugin_name: str): 

56 self.plugin_name = plugin_name 

57 

58 def get_data(self, person: Person) -> dict: 

59 return person.plugin_data.get(self.plugin_name, {}) 

60 

61 def set_data(self, person: Person, data: dict) -> Person: 

62 if not person.plugin_data: 

63 person.plugin_data = {} 

64 person.plugin_data[self.plugin_name] = data 

65 return person 

66 

67 def create(self, person: Person, data: dict) -> Person: 

68 return self.set_data(person, data) 

69 

70 def update(self, person: Person, data: dict) -> Person: 

71 return self.set_data(person, data) 

72 

73 

74class SimpleJsonForm(forms.Form): 

75 plugin_data = forms.JSONField(widget=forms.Textarea) 

76 

77 

78class SimpleAdmin: 

79 admin_template = "django_resume/admin/simple_plugin_admin_view.html" 

80 change_form = "django_resume/admin/simple_plugin_admin_form.html" 

81 

82 def __init__( 

83 self, 

84 *, 

85 plugin_name: str, 

86 plugin_verbose_name, 

87 form_class: type[forms.Form], 

88 data: SimpleData, 

89 ): 

90 self.plugin_name = plugin_name 

91 self.plugin_verbose_name = plugin_verbose_name 

92 self.form_class = form_class 

93 self.data = data 

94 

95 def get_change_url(self, person_id): 

96 return reverse( 

97 f"admin:{self.plugin_name}-admin-change", kwargs={"person_id": person_id} 

98 ) 

99 

100 def get_admin_link(self, person_id): 

101 url = self.get_change_url(person_id) 

102 return format_html( 

103 '<a href="{}">{}</a>', url, f"Edit {self.plugin_verbose_name}" 

104 ) 

105 

106 def get_change_post_url(self, person_id): 

107 return reverse( 

108 f"admin:{self.plugin_name}-admin-post", kwargs={"person_id": person_id} 

109 ) 

110 

111 def get_change_view(self, request, person_id): 

112 person = get_object_or_404(Person, pk=person_id) 

113 plugin_data = self.data.get_data(person) 

114 if self.form_class == SimpleJsonForm: 

115 # special case for the SimpleJsonForm which has a JSONField for the plugin data 

116 form = self.form_class(initial={"plugin_data": plugin_data}) 

117 else: 

118 form = self.form_class(initial=plugin_data) 

119 form.post_url = self.get_change_post_url(person.pk) 

120 context = { 

121 "title": f"{self.plugin_verbose_name} for {person.name}", 

122 "person": person, 

123 "opts": Person._meta, 

124 "form": form, 

125 "form_template": self.change_form, 

126 # context for admin/change_form.html template 

127 "add": False, 

128 "change": True, 

129 "is_popup": False, 

130 "save_as": False, 

131 "has_add_permission": False, 

132 "has_view_permission": True, 

133 "has_change_permission": True, 

134 "has_delete_permission": False, 

135 "has_editable_inline_admin_formsets": False, 

136 } 

137 return render(request, self.admin_template, context) 

138 

139 def post_view(self, request, person_id): 

140 person = get_object_or_404(Person, id=person_id) 

141 form = self.form_class(request.POST) 

142 form.post_url = self.get_change_post_url(person.pk) 

143 context = {"form": form} 

144 if form.is_valid(): 

145 if self.form_class == SimpleJsonForm: 

146 # special case for the SimpleJsonForm which has a JSONField for the plugin data 

147 plugin_data = form.cleaned_data["plugin_data"] 

148 else: 

149 plugin_data = form.cleaned_data 

150 person = self.data.update(person, plugin_data) 

151 person.save() 

152 response = render(request, self.change_form, context) 

153 return response 

154 

155 def get_urls(self, admin_view: Callable) -> URLPatterns: 

156 """ 

157 This method should return a list of urls that are used to manage the 

158 plugin data in the admin interface. 

159 """ 

160 plugin_name = self.plugin_name 

161 urls = [ 

162 path( 

163 f"<int:person_id>/plugin/{plugin_name}/change/", 

164 admin_view(self.get_change_view), 

165 name=f"{plugin_name}-admin-change", 

166 ), 

167 path( 

168 f"<int:person_id>/plugin/{plugin_name}/post/", 

169 admin_view(self.post_view), 

170 name=f"{plugin_name}-admin-post", 

171 ), 

172 ] 

173 return urls 

174 

175 

176class SimpleTemplates: 

177 def __init__(self, *, main: str, form: str): 

178 self.main = main 

179 self.form = form 

180 

181 

182class SimpleInline: 

183 def __init__( 

184 self, 

185 *, 

186 plugin_name: str, 

187 plugin_verbose_name: str, 

188 form_class: type[forms.Form], 

189 data: SimpleData, 

190 templates: SimpleTemplates, 

191 ): 

192 self.plugin_name = plugin_name 

193 self.plugin_verbose_name = plugin_verbose_name 

194 self.form_class = form_class 

195 self.data = data 

196 self.templates = templates 

197 

198 def get_edit_url(self, person_id): 

199 return reverse( 

200 f"django_resume:{self.plugin_name}-edit", kwargs={"person_id": person_id} 

201 ) 

202 

203 def get_post_url(self, person_id): 

204 return reverse( 

205 f"django_resume:{self.plugin_name}-post", kwargs={"person_id": person_id} 

206 ) 

207 

208 def get_edit_view(self, request, person_id): 

209 person = get_object_or_404(Person, id=person_id) 

210 plugin_data = self.data.get_data(person) 

211 form = self.form_class(initial=plugin_data) 

212 form.post_url = self.get_post_url(person.pk) 

213 context = {"form": form} 

214 return render(request, self.templates.form, context) 

215 

216 def post_view(self, request, person_id): 

217 person = get_object_or_404(Person, id=person_id) 

218 form_class = self.form_class 

219 form = form_class(request.POST) 

220 form.post_url = self.get_post_url(person.pk) 

221 context = {"form": form} 

222 if form.is_valid(): 

223 # update the plugin data and render the main template 

224 person = self.data.update(person, form.cleaned_data) 

225 person.save() 

226 context["show_edit_button"] = True 

227 context[self.plugin_name] = form.cleaned_data 

228 context[self.plugin_name]["edit_url"] = self.get_edit_url(person.pk) 

229 return render(request, self.templates.main, context) 

230 # render the form again with errors 

231 return render(request, self.templates.form, context) 

232 

233 def get_urls(self): 

234 plugin_name = self.plugin_name 

235 urls = [ 

236 # flat 

237 path( 

238 f"<int:person_id>/plugin/{plugin_name}/edit/", 

239 self.get_edit_view, 

240 name=f"{plugin_name}-edit", 

241 ), 

242 path( 

243 f"<int:person_id>/plugin/{plugin_name}/edit/post/", 

244 self.post_view, 

245 name=f"{plugin_name}-post", 

246 ), 

247 ] 

248 return urls 

249 

250 

251class SimplePlugin: 

252 """ 

253 A simple plugin that only stores a json serializable dict of data. It's simple, 

254 because there is only one form for the plugin data and no items with IDs or other 

255 complex logic. 

256 """ 

257 

258 name = "simple_plugin" 

259 verbose_name = "Simple Plugin" 

260 templates: SimpleTemplates = SimpleTemplates( 

261 # those two templates are just a dummies - overwrite them 

262 main="django_resume/plain/simple_plugin.html", 

263 form="django_resume/plain/simple_plugin_form.html", 

264 ) 

265 

266 def __init__(self): 

267 super().__init__() 

268 self.data = data = SimpleData(plugin_name=self.name) 

269 self.admin = SimpleAdmin( 

270 plugin_name=self.name, 

271 plugin_verbose_name=self.verbose_name, 

272 form_class=self.get_admin_form_class(), 

273 data=data, 

274 ) 

275 self.inline = SimpleInline( 

276 plugin_name=self.name, 

277 plugin_verbose_name=self.verbose_name, 

278 form_class=self.get_inline_form_class(), 

279 data=data, 

280 templates=self.templates, 

281 ) 

282 

283 def get_context(self, plugin_data, person_pk, *, context: dict[str, Any]) -> dict: 

284 """This method returns the context of the plugin for inline editing.""" 

285 if plugin_data == {}: 

286 # no data yet, use initial data from inline form 

287 form = self.get_inline_form_class()() 

288 initial_values = { 

289 field_name: form.get_initial_for_field(field, field_name) 

290 for field_name, field in form.fields.items() 

291 } 

292 plugin_data = initial_values 

293 context.update(plugin_data) 

294 context["edit_url"] = self.inline.get_edit_url(person_pk) 

295 context["templates"] = self.templates 

296 return context 

297 

298 # plugin protocol methods 

299 

300 def get_admin_form_class(self) -> type[forms.Form]: 

301 """Set admin_form_class attribute or overwrite this method.""" 

302 if hasattr(self, "admin_form_class"): 

303 return self.admin_form_class 

304 return SimpleJsonForm # default 

305 

306 def get_inline_form_class(self) -> type[forms.Form]: 

307 """Set inline_form_class attribute or overwrite this method.""" 

308 if hasattr(self, "inline_form_class"): 

309 return self.inline_form_class 

310 return SimpleJsonForm # default 

311 

312 def get_admin_urls(self, admin_view: Callable) -> URLPatterns: 

313 return self.admin.get_urls(admin_view) 

314 

315 def get_admin_link(self, person_id: int) -> str: 

316 return self.admin.get_admin_link(person_id) 

317 

318 def get_inline_urls(self) -> URLPatterns: 

319 return self.inline.get_urls() 

320 

321 def get_data(self, person: Person) -> dict: 

322 return self.data.get_data(person) 

323 

324 

325class ListItemFormMixin(forms.Form): 

326 id = forms.CharField(widget=forms.HiddenInput(), required=False) 

327 

328 def __init__(self, *args, **kwargs): 

329 self.person = kwargs.pop("person") 

330 self.existing_items = kwargs.pop("existing_items", []) 

331 super().__init__(*args, **kwargs) 

332 

333 @property 

334 def is_new(self): 

335 """Used to determine if the form is for a new item or an existing one.""" 

336 if self.is_bound: 

337 return False 

338 return not self.initial.get("id", False) 

339 

340 @property 

341 def item_id(self): 

342 """ 

343 Use an uuid for the item id if there is no id in the initial data. This is to 

344 allow the htmx delete button to work even when there are multiple new item 

345 forms on the page. 

346 """ 

347 if self.is_bound: 

348 return self.cleaned_data.get("id", uuid4()) 

349 if self.initial.get("id") is None: 

350 self.initial["id"] = uuid4() 

351 return self.initial["id"] 

352 

353 

354class ListTemplates: 

355 def __init__( 

356 self, *, main: str, flat: str, flat_form: str, item: str, item_form: str 

357 ): 

358 self.main = main 

359 self.flat = flat 

360 self.flat_form = flat_form 

361 self.item = item 

362 self.item_form = item_form 

363 

364 

365class ListData: 

366 """ 

367 This class contains the logic of the list plugin concerned with the data handling. 

368 

369 Simple crud operations are supported. 

370 """ 

371 

372 def __init__(self, *, plugin_name: str): 

373 self.plugin_name = plugin_name 

374 

375 # read 

376 def get_data(self, person: Person): 

377 return person.plugin_data.get(self.plugin_name, {}) 

378 

379 def get_item_by_id(self, person: Person, item_id: str) -> dict | None: 

380 items = self.get_data(person).get("items", []) 

381 for item in items: 

382 if item["id"] == item_id: 

383 return item 

384 return None 

385 

386 # write 

387 def set_data(self, person: Person, data: dict): 

388 if not person.plugin_data: 

389 person.plugin_data = {} 

390 person.plugin_data[self.plugin_name] = data 

391 return person 

392 

393 def create(self, person: Person, data: dict): 

394 """Create an item in the items list of this plugin.""" 

395 plugin_data = self.get_data(person) 

396 plugin_data.setdefault("items", []).append(data) 

397 person = self.set_data(person, plugin_data) 

398 return person 

399 

400 def update(self, person: Person, data: dict): 

401 """Update an item in the items list of this plugin.""" 

402 plugin_data = self.get_data(person) 

403 items = plugin_data.get("items", []) 

404 print(items, data) 

405 for item in items: 

406 if item["id"] == data["id"]: 

407 item.update(data) 

408 break 

409 plugin_data["items"] = items 

410 return self.set_data(person, plugin_data) 

411 

412 def update_flat(self, person: Person, data: dict): 

413 """Update the flat data of this plugin.""" 

414 plugin_data = self.get_data(person) 

415 plugin_data["flat"] = data 

416 return self.set_data(person, plugin_data) 

417 

418 def delete(self, person: Person, data: dict): 

419 """Delete an item from the items list of this plugin.""" 

420 plugin_data = self.get_data(person) 

421 items = plugin_data.get("items", []) 

422 for i, item in enumerate(items): 

423 if item["id"] == data["id"]: 

424 items.pop(i) 

425 break 

426 plugin_data["items"] = items 

427 return self.set_data(person, plugin_data) 

428 

429 

430class ListAdmin: 

431 """ 

432 This class contains the logic of the list plugin concerned with the Django admin interface. 

433 

434 Simple crud operations are supported. Each item in the list is a json serializable 

435 dict and should have an "id" field. 

436 

437 Why have an own class for this? Because the admin interface is different from the 

438 inline editing on the website itself. For example: the admin interface has a change 

439 view where all forms are displayed at once. Which makes sense, because the admin is 

440 for editing. 

441 """ 

442 

443 admin_change_form_template = ( 

444 "django_resume/admin/list_plugin_admin_change_form_htmx.html" 

445 ) 

446 admin_item_change_form_template = ( 

447 "django_resume/admin/list_plugin_admin_item_form.html" 

448 ) 

449 admin_flat_form_template = "django_resume/admin/list_plugin_admin_flat_form.html" 

450 

451 def __init__( 

452 self, 

453 *, 

454 plugin_name: str, 

455 plugin_verbose_name, 

456 form_classes: dict, 

457 data: ListData, 

458 ): 

459 self.plugin_name = plugin_name 

460 self.plugin_verbose_name = plugin_verbose_name 

461 self.form_classes = form_classes 

462 self.data = data 

463 

464 def get_change_url(self, person_id): 

465 """ 

466 Main admin view for this plugin. This view should display a list of item 

467 forms with update buttons for existing items and a button to get a form to 

468 add a new item. And a form to change the data for the plugin that is stored 

469 in a flat format. 

470 """ 

471 return reverse( 

472 f"admin:{self.plugin_name}-admin-change", kwargs={"person_id": person_id} 

473 ) 

474 

475 def get_admin_link(self, person_id: int) -> str: 

476 """ 

477 Return a link to the main admin view for this plugin. This is used to have the 

478 plugins show up as readonly fields in the person change view and to have a link 

479 to be able to edit the plugin data. 

480 """ 

481 url = self.get_change_url(person_id) 

482 return format_html( 

483 '<a href="{}">{}</a>', url, f"Edit {self.plugin_verbose_name}" 

484 ) 

485 

486 def get_change_flat_post_url(self, person_id): 

487 """Used for create and update flat data.""" 

488 return reverse( 

489 f"admin:{self.plugin_name}-admin-flat-post", kwargs={"person_id": person_id} 

490 ) 

491 

492 def get_change_item_post_url(self, person_id): 

493 """Used for create and update item.""" 

494 return reverse( 

495 f"admin:{self.plugin_name}-admin-item-post", kwargs={"person_id": person_id} 

496 ) 

497 

498 def get_delete_item_url(self, person_id, item_id): 

499 """Used for delete item.""" 

500 return reverse( 

501 f"admin:{self.plugin_name}-admin-item-delete", 

502 kwargs={"person_id": person_id, "item_id": item_id}, 

503 ) 

504 

505 def get_item_add_form_url(self, person_id): 

506 """ 

507 Returns the url of a view that returns a form to add a new item. The person_id 

508 is needed to be able to add the right post url to the form. 

509 """ 

510 return reverse( 

511 f"admin:{self.plugin_name}-admin-item-add", kwargs={"person_id": person_id} 

512 ) 

513 

514 # crud views 

515 

516 def get_add_item_form_view(self, request, person_id): 

517 """Return a single empty form to add a new item.""" 

518 person = get_object_or_404(Person, pk=person_id) 

519 form_class = self.form_classes["item"] 

520 existing_items = self.data.get_data(person).get("items", []) 

521 form = form_class(initial={}, person=person, existing_items=existing_items) 

522 form.post_url = self.get_change_item_post_url(person.pk) 

523 context = {"form": form} 

524 return render(request, self.admin_item_change_form_template, context) 

525 

526 def get_change_view(self, request, person_id): 

527 """Return the main admin view for this plugin.""" 

528 person = get_object_or_404(Person, pk=person_id) 

529 context = { 

530 "title": f"{self.plugin_verbose_name} for {person.name}", 

531 "person": person, 

532 "opts": Person._meta, 

533 # context for admin/change_form.html template 

534 "add": False, 

535 "change": True, 

536 "is_popup": False, 

537 "save_as": False, 

538 "has_add_permission": False, 

539 "has_view_permission": True, 

540 "has_change_permission": True, 

541 "has_delete_permission": False, 

542 "has_editable_inline_admin_formsets": False, 

543 } 

544 plugin_data = self.data.get_data(person) 

545 form_classes = self.form_classes 

546 # flat form 

547 flat_form_class = form_classes["flat"] 

548 flat_form = flat_form_class(initial=plugin_data.get("flat", {})) 

549 flat_form.post_url = self.get_change_flat_post_url(person.pk) 

550 context["flat_form"] = flat_form 

551 # item forms 

552 item_form_class = form_classes["item"] 

553 initial_items_data = plugin_data.get("items", []) 

554 post_url = self.get_change_item_post_url(person.id) 

555 item_forms = [] 

556 for initial_item_data in initial_items_data: 556 ↛ 557line 556 didn't jump to line 557 because the loop on line 556 never started

557 form = item_form_class( 

558 initial=initial_item_data, 

559 person=person, 

560 existing_items=initial_items_data, 

561 ) 

562 form.post_url = post_url 

563 form.delete_url = self.get_delete_item_url( 

564 person.id, initial_item_data["id"] 

565 ) 

566 item_forms.append(form) 

567 context["add_item_form_url"] = self.get_item_add_form_url(person.id) 

568 context["item_forms"] = item_forms 

569 return render(request, self.admin_change_form_template, context) 

570 

571 def post_item_view(self, request, person_id): 

572 """Handle post requests to create or update a single item.""" 

573 person = get_object_or_404(Person, id=person_id) 

574 form_class = self.form_classes["item"] 

575 existing_items = self.data.get_data(person).get("items", []) 

576 form = form_class(request.POST, person=person, existing_items=existing_items) 

577 form.post_url = self.get_change_item_post_url(person.pk) 

578 context = {"form": form} 

579 if form.is_valid(): 579 ↛ 594line 579 didn't jump to line 594 because the condition on line 579 was always true

580 if form.cleaned_data.get("id", False): 

581 item_id = form.cleaned_data["id"] 

582 person = self.data.update(person, form.cleaned_data) 

583 else: 

584 data = form.cleaned_data 

585 item_id = str(uuid4()) 

586 data["id"] = item_id 

587 person = self.data.create(person, data) 

588 # weird hack to make the form look like it is for an existing item 

589 # if there's a better way to do this, please let me know FIXME 

590 form.data = form.data.copy() 

591 form.data["id"] = item_id 

592 person.save() 

593 form.delete_url = self.get_delete_item_url(person.id, item_id) 

594 return render(request, self.admin_item_change_form_template, context) 

595 

596 def post_flat_view(self, request, person_id): 

597 """Handle post requests to update flat data.""" 

598 person = get_object_or_404(Person, id=person_id) 

599 form_class = self.form_classes["flat"] 

600 form = form_class(request.POST) 

601 form.post_url = self.get_change_flat_post_url(person.pk) 

602 context = {"form": form} 

603 if form.is_valid(): 603 ↛ 606line 603 didn't jump to line 606 because the condition on line 603 was always true

604 person = self.data.update_flat(person, form.cleaned_data) 

605 person.save() 

606 return render(request, self.admin_flat_form_template, context) 

607 

608 def delete_item_view(self, _request, person_id, item_id): 

609 """Delete an item from the items list of this plugin.""" 

610 person = get_object_or_404(Person, pk=person_id) 

611 person = self.data.delete(person, {"id": item_id}) 

612 person.save() 

613 return HttpResponse(status=200) 

614 

615 # urlpatterns 

616 

617 def get_urls(self, admin_view: Callable) -> URLPatterns: 

618 """ 

619 This method should return a list of urls that are used to manage the 

620 plugin data in the admin interface. 

621 """ 

622 plugin_name = self.plugin_name 

623 urls = [ 

624 path( 

625 f"<int:person_id>/plugin/{plugin_name}/change/", 

626 admin_view(self.get_change_view), 

627 name=f"{plugin_name}-admin-change", 

628 ), 

629 path( 

630 f"<int:person_id>/plugin/{plugin_name}/item/post/", 

631 admin_view(self.post_item_view), 

632 name=f"{plugin_name}-admin-item-post", 

633 ), 

634 path( 

635 f"<int:person_id>/plugin/{plugin_name}/add/", 

636 admin_view(self.get_add_item_form_view), 

637 name=f"{plugin_name}-admin-item-add", 

638 ), 

639 path( 

640 f"<int:person_id>/plugin/{plugin_name}/delete/<str:item_id>/", 

641 admin_view(self.delete_item_view), 

642 name=f"{plugin_name}-admin-item-delete", 

643 ), 

644 path( 

645 f"<int:person_id>/plugin/{plugin_name}/flat/post/", 

646 admin_view(self.post_flat_view), 

647 name=f"{plugin_name}-admin-flat-post", 

648 ), 

649 ] 

650 return urls 

651 

652 

653class ListInline: 

654 """ 

655 This class contains the logic of the list plugin concerned with the inline editing 

656 of the plugin data on the website itself. 

657 """ 

658 

659 def __init__( 

660 self, 

661 *, 

662 plugin_name: str, 

663 plugin_verbose_name: str, 

664 form_classes: dict, 

665 data: ListData, 

666 templates: ListTemplates, 

667 ): 

668 self.plugin_name = plugin_name 

669 self.plugin_verbose_name = plugin_verbose_name 

670 self.form_classes = form_classes 

671 self.data = data 

672 self.templates = templates 

673 

674 # urls 

675 

676 def get_edit_flat_post_url(self, person_id): 

677 return reverse( 

678 f"django_resume:{self.plugin_name}-edit-flat-post", 

679 kwargs={"person_id": person_id}, 

680 ) 

681 

682 def get_edit_flat_url(self, person_id): 

683 return reverse( 

684 f"django_resume:{self.plugin_name}-edit-flat", 

685 kwargs={"person_id": person_id}, 

686 ) 

687 

688 def get_edit_item_url(self, person_id, item_id=None): 

689 if item_id is None: 

690 return reverse( 

691 f"django_resume:{self.plugin_name}-add-item", 

692 kwargs={"person_id": person_id}, 

693 ) 

694 else: 

695 return reverse( 

696 f"django_resume:{self.plugin_name}-edit-item", 

697 kwargs={"person_id": person_id, "item_id": item_id}, 

698 ) 

699 

700 def get_post_item_url(self, person_id): 

701 return reverse( 

702 f"django_resume:{self.plugin_name}-item-post", 

703 kwargs={"person_id": person_id}, 

704 ) 

705 

706 def get_delete_item_url(self, person_id, item_id): 

707 return reverse( 

708 f"django_resume:{self.plugin_name}-delete-item", 

709 kwargs={"person_id": person_id, "item_id": item_id}, 

710 ) 

711 

712 # crud views 

713 

714 def get_edit_flat_view(self, request, person_id): 

715 person = get_object_or_404(Person, id=person_id) 

716 plugin_data = self.data.get_data(person) 

717 flat_form_class = self.form_classes["flat"] 

718 flat_form = flat_form_class(initial=plugin_data.get("flat", {})) 

719 flat_form.post_url = self.get_edit_flat_post_url(person.pk) 

720 context = { 

721 "form": flat_form, 

722 "edit_flat_post_url": self.get_edit_flat_post_url(person.pk), 

723 } 

724 return render(request, self.templates.flat_form, context=context) 

725 

726 def post_edit_flat_view(self, request, person_id): 

727 person = get_object_or_404(Person, id=person_id) 

728 flat_form_class = self.form_classes["flat"] 

729 plugin_data = self.data.get_data(person) 

730 flat_form = flat_form_class(request.POST, initial=plugin_data.get("flat", {})) 

731 context = {} 

732 if flat_form.is_valid(): 

733 person = self.data.update_flat(person, flat_form.cleaned_data) 

734 person.save() 

735 person.refresh_from_db() 

736 plugin_data = self.data.get_data(person) 

737 context["edit_flat_url"] = self.get_edit_flat_url(person.pk) 

738 context = flat_form.set_context(plugin_data["flat"], context) 

739 context["show_edit_button"] = True 

740 return render(request, self.templates.flat, context=context) 

741 else: 

742 context["form"] = flat_form 

743 context["edit_flat_post_url"] = self.get_edit_flat_post_url(person.pk) 

744 response = render(request, self.templates.flat_form, context=context) 

745 return response 

746 

747 def get_item_view(self, request, person_id, item_id=None): 

748 person = get_object_or_404(Person, id=person_id) 

749 plugin_data = self.data.get_data(person) 

750 existing_items = plugin_data.get("items", []) 

751 form_class = self.form_classes["item"] 

752 # get the item data if we are editing an existing item 

753 initial = form_class.get_initial() 

754 if item_id is not None: 

755 for item in existing_items: 

756 if item["id"] == item_id: 756 ↛ 755line 756 didn't jump to line 755 because the condition on line 756 was always true

757 initial = item 

758 form = form_class(initial=initial, person=person, existing_items=existing_items) 

759 form.post_url = self.get_post_item_url(person.pk) 

760 context = {"form": form} 

761 return render(request, self.templates.item_form, context=context) 

762 

763 def post_item_view(self, request, person_id): 

764 print("in post item view!") 

765 person = get_object_or_404(Person, id=person_id) 

766 form_class = self.form_classes["item"] 

767 existing_items = self.data.get_data(person).get("items", []) 

768 form = form_class(request.POST, person=person, existing_items=existing_items) 

769 form.post_url = self.get_post_item_url(person.pk) 

770 context = {"form": form} 

771 if form.is_valid(): 771 ↛ 807line 771 didn't jump to line 807 because the condition on line 771 was always true

772 # try to find out whether we are updating an existing item or creating a new one 

773 existing = True 

774 item_id = form.cleaned_data.get("id", None) 

775 if item_id is not None: 775 ↛ 781line 775 didn't jump to line 781 because the condition on line 775 was always true

776 item = self.data.get_item_by_id(person, item_id) 

777 if item is None: 

778 existing = False 

779 else: 

780 # no item_id -> new item 

781 existing = False 

782 if existing: 

783 # update existing item 

784 item_id = form.cleaned_data["id"] 

785 person = self.data.update(person, form.cleaned_data) 

786 else: 

787 # create new item 

788 data = form.cleaned_data 

789 item_id = str(uuid4()) 

790 data["id"] = item_id 

791 person = self.data.create(person, data) 

792 # weird hack to make the form look like it is for an existing item 

793 # if there's a better way to do this, please let me know FIXME 

794 form.data = form.data.copy() 

795 form.data["id"] = item_id 

796 person.save() 

797 item = self.data.get_item_by_id(person, item_id) 

798 # populate entry because it's used in the standard item template, 

799 # and we are no longer rendering a form when the form was valid 

800 context["edit_url"] = self.get_edit_item_url(person.id, item_id) 

801 context["delete_url"] = self.get_delete_item_url(person.id, item_id) 

802 form.set_context(item, context) 

803 context["show_edit_button"] = True 

804 return render(request, self.templates.item, context) 

805 else: 

806 # form is invalid 

807 return render(request, self.templates.item_form, context) 

808 

809 def delete_item_view(self, _request, person_id, item_id): 

810 """Delete an item from the items list of this plugin.""" 

811 person = get_object_or_404(Person, pk=person_id) 

812 person = self.data.delete(person, {"id": item_id}) 

813 person.save() 

814 return HttpResponse(status=200) 

815 

816 # urlpatterns 

817 def get_urls(self): 

818 plugin_name = self.plugin_name 

819 urls = [ 

820 # flat 

821 path( 

822 f"<int:person_id>/plugin/{plugin_name}/edit/flat/", 

823 self.get_edit_flat_view, 

824 name=f"{plugin_name}-edit-flat", 

825 ), 

826 path( 

827 f"<int:person_id>/plugin/{plugin_name}/edit/flat/post/", 

828 self.post_edit_flat_view, 

829 name=f"{plugin_name}-edit-flat-post", 

830 ), 

831 # item 

832 path( 

833 f"<int:person_id>/plugin/{plugin_name}/edit/item/<str:item_id>", 

834 self.get_item_view, 

835 name=f"{plugin_name}-edit-item", 

836 ), 

837 path( 

838 f"<int:person_id>/plugin/{plugin_name}/edit/item/", 

839 self.get_item_view, 

840 name=f"{plugin_name}-add-item", 

841 ), 

842 path( 

843 f"<int:person_id>/plugin/{plugin_name}/edit/item/post/", 

844 self.post_item_view, 

845 name=f"{plugin_name}-item-post", 

846 ), 

847 path( 

848 f"<int:person_id>/plugin/{plugin_name}/delete/<str:item_id>/", 

849 self.delete_item_view, 

850 name=f"{plugin_name}-delete-item", 

851 ), 

852 ] 

853 return urls 

854 

855 

856class ListPlugin: 

857 """ 

858 A plugin that displays a list of items. Simple crud operations are supported. 

859 Each item in the list is a json serializable dict and should have an "id" field. 

860 

861 Additional flat data can be stored in the plugin_data['flat'] field. 

862 """ 

863 

864 name = "list_plugin" 

865 verbose_name = "List Plugin" 

866 templates: ListTemplates = ListTemplates( 

867 main="", flat="", flat_form="", item="", item_form="" 

868 ) # overwrite this 

869 

870 def __init__(self): 

871 super().__init__() 

872 self.data = data = ListData(plugin_name=self.name) 

873 form_classes = self.get_form_classes() 

874 self.admin = ListAdmin( 

875 plugin_name=self.name, 

876 plugin_verbose_name=self.verbose_name, 

877 form_classes=form_classes, 

878 data=data, 

879 ) 

880 self.inline = ListInline( 

881 plugin_name=self.name, 

882 plugin_verbose_name=self.verbose_name, 

883 form_classes=form_classes, 

884 data=data, 

885 templates=self.templates, 

886 ) 

887 

888 # main interface see Plugin class 

889 def get_admin_urls(self, admin_view: Callable) -> URLPatterns: 

890 return self.admin.get_urls(admin_view) 

891 

892 def get_admin_link(self, person_id: int) -> str: 

893 return self.admin.get_admin_link(person_id) 

894 

895 def get_inline_urls(self) -> URLPatterns: 

896 return self.inline.get_urls() 

897 

898 def get_form_classes(self) -> dict[str, type[forms.Form]]: 

899 """Please implement this method.""" 

900 return {} 

901 

902 def get_data(self, person: Person) -> dict: 

903 return self.data.get_data(person)