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

1 from uuid import uuid4

2

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

4

5 from django import forms

6 from django.http import HttpResponse

7 from django.shortcuts import get_object_or_404, render

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

9 from django.utils.html import format_html

10

11 from .models import Person

12

13

14 URLPatterns: TypeAlias = list[URLPattern | URLResolver]

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

16

17

18@runtime_checkable

19 class 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

54 class 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

74 class SimpleJsonForm(forms.Form):

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

76

77

78 class 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

176 class SimpleTemplates:

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

178 self.main = main

179 self.form = form

180

181

182 class 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

251 class 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

325 class 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

354 class 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

365 class 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

430 class 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

653 class 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

856 class 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)