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
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-27 17:54 +0200
1from uuid import uuid4
3from typing import Protocol, runtime_checkable, Callable, TypeAlias, Any
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
11from .models import Person
14URLPatterns: TypeAlias = list[URLPattern | URLResolver]
15FormClasses: TypeAlias = dict[str, type[forms.Form]]
18@runtime_checkable
19class Plugin(Protocol):
20 name: str
21 verbose_name: str
22 form_classes: FormClasses
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
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
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
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
43 def get_data(self, person: Person) -> dict:
44 """Return the plugin data for a person."""
45 ... # pragma: no cover
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
54class SimpleData:
55 def __init__(self, *, plugin_name: str):
56 self.plugin_name = plugin_name
58 def get_data(self, person: Person) -> dict:
59 return person.plugin_data.get(self.plugin_name, {})
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
67 def create(self, person: Person, data: dict) -> Person:
68 return self.set_data(person, data)
70 def update(self, person: Person, data: dict) -> Person:
71 return self.set_data(person, data)
74class SimpleJsonForm(forms.Form):
75 plugin_data = forms.JSONField(widget=forms.Textarea)
78class SimpleAdmin:
79 admin_template = "django_resume/admin/simple_plugin_admin_view.html"
80 change_form = "django_resume/admin/simple_plugin_admin_form.html"
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
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 )
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 )
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 )
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)
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
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
176class SimpleTemplates:
177 def __init__(self, *, main: str, form: str):
178 self.main = main
179 self.form = form
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
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 )
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 )
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)
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)
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
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 """
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 )
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 )
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
298 # plugin protocol methods
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
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
312 def get_admin_urls(self, admin_view: Callable) -> URLPatterns:
313 return self.admin.get_urls(admin_view)
315 def get_admin_link(self, person_id: int) -> str:
316 return self.admin.get_admin_link(person_id)
318 def get_inline_urls(self) -> URLPatterns:
319 return self.inline.get_urls()
321 def get_data(self, person: Person) -> dict:
322 return self.data.get_data(person)
325class ListItemFormMixin(forms.Form):
326 id = forms.CharField(widget=forms.HiddenInput(), required=False)
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)
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)
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"]
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
365class ListData:
366 """
367 This class contains the logic of the list plugin concerned with the data handling.
369 Simple crud operations are supported.
370 """
372 def __init__(self, *, plugin_name: str):
373 self.plugin_name = plugin_name
375 # read
376 def get_data(self, person: Person):
377 return person.plugin_data.get(self.plugin_name, {})
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
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
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
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)
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)
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)
430class ListAdmin:
431 """
432 This class contains the logic of the list plugin concerned with the Django admin interface.
434 Simple crud operations are supported. Each item in the list is a json serializable
435 dict and should have an "id" field.
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 """
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"
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
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 )
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 )
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 )
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 )
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 )
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 )
514 # crud views
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)
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)
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)
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)
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)
615 # urlpatterns
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
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 """
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
674 # urls
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 )
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 )
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 )
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 )
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 )
712 # crud views
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)
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
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)
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)
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)
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
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.
861 Additional flat data can be stored in the plugin_data['flat'] field.
862 """
864 name = "list_plugin"
865 verbose_name = "List Plugin"
866 templates: ListTemplates = ListTemplates(
867 main="", flat="", flat_form="", item="", item_form=""
868 ) # overwrite this
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 )
888 # main interface see Plugin class
889 def get_admin_urls(self, admin_view: Callable) -> URLPatterns:
890 return self.admin.get_urls(admin_view)
892 def get_admin_link(self, person_id: int) -> str:
893 return self.admin.get_admin_link(person_id)
895 def get_inline_urls(self) -> URLPatterns:
896 return self.inline.get_urls()
898 def get_form_classes(self) -> dict[str, type[forms.Form]]:
899 """Please implement this method."""
900 return {}
902 def get_data(self, person: Person) -> dict:
903 return self.data.get_data(person)