django-spire 0.22.4__py3-none-any.whl → 0.23.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. django_spire/auth/group/forms.py +3 -4
  2. django_spire/auth/group/utils.py +1 -2
  3. django_spire/auth/group/views/form_views.py +22 -14
  4. django_spire/auth/group/views/page_views.py +1 -1
  5. django_spire/auth/mfa/utils.py +1 -1
  6. django_spire/auth/permissions/consts.py +2 -1
  7. django_spire/auth/permissions/decorators.py +1 -2
  8. django_spire/auth/permissions/permissions.py +1 -3
  9. django_spire/comment/factories.py +1 -1
  10. django_spire/comment/mixins.py +1 -1
  11. django_spire/comment/views.py +1 -1
  12. django_spire/consts.py +1 -1
  13. django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
  14. django_spire/contrib/form/confirmation_forms.py +1 -1
  15. django_spire/contrib/form/utils.py +26 -16
  16. django_spire/contrib/generic_views/modal_views.py +1 -1
  17. django_spire/contrib/generic_views/portal_views.py +18 -55
  18. django_spire/contrib/ordering/validators.py +4 -8
  19. django_spire/contrib/queryset/enums.py +2 -3
  20. django_spire/contrib/queryset/mixins.py +17 -6
  21. django_spire/contrib/session/controller.py +0 -1
  22. django_spire/core/context_processors.py +1 -1
  23. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
  24. django_spire/core/middleware/maintenance.py +2 -1
  25. django_spire/core/middleware.py +2 -1
  26. django_spire/core/redirect/generic_redirect.py +1 -1
  27. django_spire/core/redirect/safe_redirect.py +2 -1
  28. django_spire/core/shortcuts.py +4 -2
  29. django_spire/core/table/__init__.py +0 -0
  30. django_spire/core/table/enums.py +18 -0
  31. django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
  32. django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
  33. django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
  34. django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
  35. django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
  36. django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
  37. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
  38. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
  39. django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
  40. django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
  41. django_spire/core/templates/django_spire/table/base.html +185 -373
  42. django_spire/core/templates/django_spire/table/element/footer.html +7 -15
  43. django_spire/core/templates/django_spire/table/element/header.html +1 -1
  44. django_spire/core/templates/django_spire/table/element/row.html +15 -7
  45. django_spire/core/templatetags/spire_core_tags.py +1 -2
  46. django_spire/file/fields.py +1 -1
  47. django_spire/file/interfaces.py +1 -1
  48. django_spire/file/views.py +1 -1
  49. django_spire/history/activity/utils.py +1 -1
  50. django_spire/history/mixins.py +0 -4
  51. django_spire/notification/app/context_data.py +3 -1
  52. django_spire/notification/app/views/json_views.py +1 -1
  53. django_spire/notification/app/views/page_views.py +2 -1
  54. django_spire/notification/app/views/template_views.py +2 -2
  55. django_spire/profiling/middleware/profiling.py +2 -2
  56. django_spire/profiling/panel.py +2 -2
  57. django_spire/testing/__init__.py +0 -0
  58. django_spire/testing/playwright/__init__.py +64 -0
  59. django_spire/testing/playwright/components/__init__.py +45 -0
  60. django_spire/testing/playwright/components/accordion.py +55 -0
  61. django_spire/testing/playwright/components/attribute_element.py +73 -0
  62. django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
  63. django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
  64. django_spire/testing/playwright/components/card.py +102 -0
  65. django_spire/testing/playwright/components/dropdown.py +87 -0
  66. django_spire/testing/playwright/components/infinite_scroll.py +158 -0
  67. django_spire/testing/playwright/components/lazy_tab.py +92 -0
  68. django_spire/testing/playwright/components/modal.py +101 -0
  69. django_spire/testing/playwright/components/navigation.py +119 -0
  70. django_spire/testing/playwright/components/notification_bell.py +59 -0
  71. django_spire/testing/playwright/components/theme_selector.py +46 -0
  72. django_spire/testing/playwright/components/toast.py +72 -0
  73. django_spire/testing/playwright/fixtures.py +54 -0
  74. django_spire/testing/playwright/pages/__init__.py +6 -0
  75. django_spire/testing/playwright/pages/base.py +24 -0
  76. django_spire/theme/models.py +1 -1
  77. django_spire/theme/tests/test_context_processor.py +0 -1
  78. django_spire/theme/views/json_views.py +1 -1
  79. django_spire/theme/views/page_views.py +1 -1
  80. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
  81. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
  82. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
  83. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
  84. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,11 @@
3
3
  x-data="{
4
4
  init() {
5
5
  let table_id = this.$el.closest('[data-table-id]').dataset.tableId;
6
- this.$dispatch('total-count-updated', { total_count: {{ total_count }}, batch_size: {{ batch_size|default:25 }}, table_id: table_id });
6
+ this.$dispatch('table-total-count-updated', {
7
+ batch_size: {{ batch_size|default:25 }},
8
+ table_id: table_id,
9
+ total_count: {{ total_count }}
10
+ });
7
11
  }
8
12
  }"
9
13
  style="display: none;"
@@ -11,18 +15,22 @@
11
15
  {% endif %}
12
16
 
13
17
  <tr
14
- class="align-middle {% block unified_row_background_colour %}{% endblock %}"
18
+ class="align-middle {% block row_class %}{% endblock %}"
15
19
  data-row-id="{{ row.pk }}"
16
20
  x-data="{
17
- row_id: '{{ row.pk }}',
18
- table_id: null,
21
+ has_children: false,
19
22
  is_checked: false,
20
23
  is_open: false,
21
- has_children: false,
24
+ row_id: '{{ row.pk }}',
25
+ table_id: null,
22
26
 
23
27
  init() {
24
28
  this.table_id = this.$el.closest('[data-table-id]').dataset.tableId;
25
- this.$dispatch('row-mounted', { row_id: this.row_id, row_element: this.$el, table_id: this.table_id });
29
+ this.$dispatch('row-mounted', {
30
+ row_element: this.$el,
31
+ row_id: this.row_id,
32
+ table_id: this.table_id
33
+ });
26
34
  },
27
35
 
28
36
  handle_checkbox_change() {
@@ -84,7 +92,7 @@
84
92
  </template>
85
93
  </td>
86
94
 
87
- {% block table_cells %}{% endblock %}
95
+ {% block table_cell %}{% endblock %}
88
96
  </tr>
89
97
 
90
98
  <tr class="align-middle" data-row-id="{{ row.pk }}">
@@ -2,9 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  import string
5
- from typing import Any
6
5
 
7
- from typing_extensions import Sequence, TYPE_CHECKING, TypeVar
6
+ from typing import Any, Sequence, TYPE_CHECKING, TypeVar
8
7
 
9
8
  from django import template
10
9
  from django.db.models import Model
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
 
5
- from typing_extensions import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from django import forms
8
8
 
@@ -2,10 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  from django.conf import settings
7
8
  from django.core.files.base import ContentFile
8
- from typing_extensions import TYPE_CHECKING
9
9
 
10
10
  from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.exceptions import ObjectDoesNotExist
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.contrib.auth.decorators import login_required
6
6
  from django.http import JsonResponse
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from django.contrib.auth.models import User
@@ -3,14 +3,10 @@ from __future__ import annotations
3
3
  from django.contrib.contenttypes.fields import GenericRelation
4
4
  from django.db import models
5
5
  from django.utils.timezone import localtime
6
- from typing_extensions import TYPE_CHECKING
7
6
 
8
7
  from django_spire.history.choices import HistoryEventChoices
9
8
  from django_spire.history.models import HistoryEvent
10
9
 
11
- if TYPE_CHECKING:
12
- from django.contrib.auth.models import User
13
-
14
10
 
15
11
  class HistoryModelMixin(models.Model):
16
12
  is_active = models.BooleanField(default=True, editable=False)
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
+
2
3
  import json
3
- from typing_extensions import TYPE_CHECKING
4
+
5
+ from typing import TYPE_CHECKING
4
6
 
5
7
  from django_spire.notification.app.models import AppNotification
6
8
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
- from typing_extensions import TYPE_CHECKING
3
2
 
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.http import JsonResponse
6
6
  from django.contrib.contenttypes.models import ContentType
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.contrib.auth.decorators import login_required
4
- from typing_extensions import TYPE_CHECKING
5
6
 
6
7
  from django_spire.contrib.generic_views import portal_views
7
8
 
@@ -2,9 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
 
5
- from django.contrib.auth.models import AnonymousUser
6
- from typing_extensions import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING
7
6
 
7
+ from django.contrib.auth.models import AnonymousUser
8
8
  from django.template.response import TemplateResponse
9
9
 
10
10
  from django_spire.notification.app.models import AppNotification
@@ -5,7 +5,7 @@ import threading
5
5
  import time
6
6
 
7
7
  from pathlib import Path
8
- from typing_extensions import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  from django.conf import settings
11
11
  from django.utils.deprecation import MiddlewareMixin
@@ -18,7 +18,7 @@ except ImportError:
18
18
  Profiler = None
19
19
 
20
20
  if TYPE_CHECKING:
21
- from typing_extensions import Any, Callable
21
+ from typing import Any, Callable
22
22
 
23
23
  from django.http import HttpRequest, HttpResponse
24
24
 
@@ -6,7 +6,7 @@ import time
6
6
  from dataclasses import asdict, dataclass
7
7
  from datetime import datetime
8
8
  from pathlib import Path
9
- from typing_extensions import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING
10
10
 
11
11
  from debug_toolbar.panels import Panel
12
12
  from django.conf import settings
@@ -18,7 +18,7 @@ from django.views.decorators.csrf import csrf_exempt
18
18
  from django_spire.profiling import lock
19
19
 
20
20
  if TYPE_CHECKING:
21
- from typing_extensions import Any
21
+ from typing import Any
22
22
 
23
23
 
24
24
  @dataclass
File without changes
@@ -0,0 +1,64 @@
1
+ from django_spire.testing.playwright.components import (
2
+ Accordion,
3
+ AttributeElement,
4
+ AttributeList,
5
+ Breadcrumb,
6
+ Card,
7
+ DeleteModal,
8
+ Dropdown,
9
+ EllipsisDropdown,
10
+ EllipsisModalDropdown,
11
+ EllipsisTableDropdown,
12
+ FilterForm,
13
+ FormCard,
14
+ FormModal,
15
+ InfiniteScroll,
16
+ InfiniteScrollCard,
17
+ InfiniteScrollList,
18
+ InfiniteScrollTable,
19
+ LazyTab,
20
+ Modal,
21
+ NavAccordion,
22
+ NotificationBell,
23
+ SideNavigation,
24
+ ThemeSelector,
25
+ TitleCard,
26
+ TitleModal,
27
+ Toast,
28
+ TopNavigation,
29
+ UserMenu,
30
+ )
31
+ from django_spire.testing.playwright.pages import BasePage
32
+
33
+
34
+ __all__ = [
35
+ 'Accordion',
36
+ 'AttributeElement',
37
+ 'AttributeList',
38
+ 'BasePage',
39
+ 'Breadcrumb',
40
+ 'Card',
41
+ 'DeleteModal',
42
+ 'Dropdown',
43
+ 'EllipsisDropdown',
44
+ 'EllipsisModalDropdown',
45
+ 'EllipsisTableDropdown',
46
+ 'FilterForm',
47
+ 'FormCard',
48
+ 'FormModal',
49
+ 'InfiniteScroll',
50
+ 'InfiniteScrollCard',
51
+ 'InfiniteScrollList',
52
+ 'InfiniteScrollTable',
53
+ 'LazyTab',
54
+ 'Modal',
55
+ 'NavAccordion',
56
+ 'NotificationBell',
57
+ 'SideNavigation',
58
+ 'ThemeSelector',
59
+ 'TitleCard',
60
+ 'TitleModal',
61
+ 'Toast',
62
+ 'TopNavigation',
63
+ 'UserMenu',
64
+ ]
@@ -0,0 +1,45 @@
1
+ from django_spire.testing.playwright.components.accordion import Accordion, NavAccordion
2
+ from django_spire.testing.playwright.components.attribute_element import AttributeElement, AttributeList
3
+ from django_spire.testing.playwright.components.base_session_filter_form import FilterForm
4
+ from django_spire.testing.playwright.components.breadcrumb_element import Breadcrumb
5
+ from django_spire.testing.playwright.components.card import Card, FormCard, InfiniteScrollCard, TitleCard
6
+ from django_spire.testing.playwright.components.dropdown import Dropdown, EllipsisDropdown, EllipsisModalDropdown, EllipsisTableDropdown
7
+ from django_spire.testing.playwright.components.infinite_scroll import InfiniteScroll, InfiniteScrollList, InfiniteScrollTable
8
+ from django_spire.testing.playwright.components.lazy_tab import LazyTab
9
+ from django_spire.testing.playwright.components.modal import DeleteModal, FormModal, Modal, TitleModal
10
+ from django_spire.testing.playwright.components.navigation import SideNavigation, TopNavigation, UserMenu
11
+ from django_spire.testing.playwright.components.notification_bell import NotificationBell
12
+ from django_spire.testing.playwright.components.theme_selector import ThemeSelector
13
+ from django_spire.testing.playwright.components.toast import Toast
14
+
15
+
16
+ __all__ = [
17
+ 'Accordion',
18
+ 'AttributeElement',
19
+ 'AttributeList',
20
+ 'Breadcrumb',
21
+ 'Card',
22
+ 'DeleteModal',
23
+ 'Dropdown',
24
+ 'EllipsisDropdown',
25
+ 'EllipsisModalDropdown',
26
+ 'EllipsisTableDropdown',
27
+ 'FilterForm',
28
+ 'FormCard',
29
+ 'FormModal',
30
+ 'InfiniteScroll',
31
+ 'InfiniteScrollCard',
32
+ 'InfiniteScrollList',
33
+ 'InfiniteScrollTable',
34
+ 'LazyTab',
35
+ 'Modal',
36
+ 'NavAccordion',
37
+ 'NotificationBell',
38
+ 'SideNavigation',
39
+ 'ThemeSelector',
40
+ 'TitleCard',
41
+ 'TitleModal',
42
+ 'Toast',
43
+ 'TopNavigation',
44
+ 'UserMenu',
45
+ ]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from playwright.sync_api import Locator
7
+
8
+
9
+ class Accordion:
10
+ """Playwright component for django_spire/accordion/accordion.html"""
11
+
12
+ def __init__(self, parent_locator: Locator) -> None:
13
+ self.parent = parent_locator
14
+
15
+ @property
16
+ def content(self) -> Locator:
17
+ return self.parent.locator('[x-show="show_accordion"]')
18
+
19
+ @property
20
+ def toggle(self) -> Locator:
21
+ return self.parent.locator('[\\@click*="toggle"]').first
22
+
23
+ def close(self) -> None:
24
+ if self.is_open():
25
+ self.toggle.click()
26
+
27
+ def is_open(self) -> bool:
28
+ return self.content.is_visible()
29
+
30
+ def open(self) -> None:
31
+ if not self.is_open():
32
+ self.toggle.click()
33
+
34
+
35
+ class NavAccordion(Accordion):
36
+ """Playwright component for django_spire/navigation/accordion/nav_accordion.html"""
37
+
38
+ @property
39
+ def chevron(self) -> Locator:
40
+ return self.parent.locator('.bi-chevron-right, .bi-chevron-down')
41
+
42
+ @property
43
+ def icon(self) -> Locator:
44
+ return self.parent.locator('i.fs-6').first
45
+
46
+ @property
47
+ def title(self) -> Locator:
48
+ return self.parent.locator('span.h6')
49
+
50
+ def get_title_text(self) -> str:
51
+ return self.title.inner_text()
52
+
53
+ def is_expanded(self) -> bool:
54
+ chevron_classes = self.chevron.get_attribute('class') or ''
55
+ return 'bi-chevron-down' in chevron_classes
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from playwright.sync_api import Locator, Page
7
+
8
+
9
+ class AttributeElement:
10
+ """Playwright component for django_spire/element/attribute_element.html"""
11
+
12
+ def __init__(self, page: Page, container_selector: str = 'body') -> None:
13
+ self.container_selector = container_selector
14
+ self.page = page
15
+
16
+ @property
17
+ def container(self) -> Locator:
18
+ return self.page.locator(self.container_selector)
19
+
20
+ def get_attribute_by_title(self, title: str) -> Locator:
21
+ return self.container.locator(f'.fs-7.text-app-attribute-color:has-text("{title}")').locator('..')
22
+
23
+ def get_value_by_title(self, title: str) -> str:
24
+ attribute = self.get_attribute_by_title(title)
25
+ return attribute.locator('.fs-6, a').inner_text()
26
+
27
+ def get_value_href_by_title(self, title: str) -> str | None:
28
+ attribute = self.get_attribute_by_title(title)
29
+ link = attribute.locator('a')
30
+
31
+ if link.count() > 0:
32
+ return link.get_attribute('href')
33
+
34
+ return None
35
+
36
+ def has_attribute(self, title: str) -> bool:
37
+ return self.get_attribute_by_title(title).count() > 0
38
+
39
+ def is_value_link(self, title: str) -> bool:
40
+ attribute = self.get_attribute_by_title(title)
41
+ return attribute.locator('a').count() > 0
42
+
43
+
44
+ class AttributeList:
45
+ """Helper for pages with multiple attribute elements"""
46
+
47
+ def __init__(self, page: Page, container_selector: str = 'body') -> None:
48
+ self.container_selector = container_selector
49
+ self.page = page
50
+
51
+ @property
52
+ def container(self) -> Locator:
53
+ return self.page.locator(self.container_selector)
54
+
55
+ @property
56
+ def attributes(self) -> Locator:
57
+ return self.container.locator('.fs-7.text-app-attribute-color')
58
+
59
+ def get_all_titles(self) -> list[str]:
60
+ count = self.attributes.count()
61
+ return [self.attributes.nth(i).inner_text() for i in range(count)]
62
+
63
+ def get_attribute_count(self) -> int:
64
+ return self.attributes.count()
65
+
66
+ def get_values_dict(self) -> dict[str, str]:
67
+ result = {}
68
+ attr_element = AttributeElement(self.page, self.container_selector)
69
+
70
+ for title in self.get_all_titles():
71
+ result[title] = attr_element.get_value_by_title(title)
72
+
73
+ return result
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from playwright.sync_api import Locator, Page
7
+
8
+
9
+ class FilterForm:
10
+ """Playwright component for django_spire/filtering/form/base_session_filter_form.html"""
11
+
12
+ def __init__(self, page: Page, form_selector: str = 'form[action*="filter"], form[action*="search"]') -> None:
13
+ self.form_selector = form_selector
14
+ self.page = page
15
+
16
+ @property
17
+ def clear_button(self) -> Locator:
18
+ return self.form.locator('input[value="Clear"], button:has-text("Clear")')
19
+
20
+ @property
21
+ def filter_button(self) -> Locator:
22
+ return self.form.locator('input[value="Filter"], button:has-text("Filter")')
23
+
24
+ @property
25
+ def form(self) -> Locator:
26
+ return self.page.locator(self.form_selector).first
27
+
28
+ @property
29
+ def search_button(self) -> Locator:
30
+ return self.form.locator('input[value="Search"], button:has-text("Search")')
31
+
32
+ @property
33
+ def search_input(self) -> Locator:
34
+ return self.form.locator('input[name="search"], input[placeholder*="Search"]')
35
+
36
+ def clear(self) -> None:
37
+ self.clear_button.click()
38
+
39
+ def fill_field(self, name: str, value: str) -> None:
40
+ self.form.locator(f'[name="{name}"]').fill(value)
41
+
42
+ def filter(self) -> None:
43
+ self.filter_button.click()
44
+
45
+ def get_field_value(self, name: str) -> str:
46
+ return self.form.locator(f'[name="{name}"]').input_value()
47
+
48
+ def search(self, query: str) -> None:
49
+ self.search_input.fill(query)
50
+ self.search_button.click()
51
+
52
+ def select_option(self, name: str, value: str) -> None:
53
+ self.form.locator(f'select[name="{name}"]').select_option(value)
54
+
55
+ def submit(self) -> None:
56
+ submit_btn = self.form.locator('button[type="submit"], input[type="submit"]').first
57
+ submit_btn.click()
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from playwright.sync_api import Locator, Page
7
+
8
+
9
+ class Breadcrumb:
10
+ """Playwright component for django_spire/element/breadcrumb_element.html"""
11
+
12
+ def __init__(self, page: Page) -> None:
13
+ self.page = page
14
+
15
+ @property
16
+ def breadcrumb(self) -> Locator:
17
+ return self.page.locator('ol.breadcrumb')
18
+
19
+ @property
20
+ def items(self) -> Locator:
21
+ return self.breadcrumb.locator('.breadcrumb-item')
22
+
23
+ def click_item(self, index: int) -> None:
24
+ link = self.get_item(index).locator('a')
25
+
26
+ if link.count() > 0:
27
+ link.click()
28
+
29
+ def get_item(self, index: int) -> Locator:
30
+ return self.items.nth(index)
31
+
32
+ def get_item_count(self) -> int:
33
+ return self.items.count()
34
+
35
+ def get_item_href(self, index: int) -> str | None:
36
+ link = self.get_item(index).locator('a')
37
+
38
+ if link.count() > 0:
39
+ return link.get_attribute('href')
40
+
41
+ return None
42
+
43
+ def get_item_text(self, index: int) -> str:
44
+ return self.get_item(index).inner_text()
45
+
46
+ def get_items_text(self) -> list[str]:
47
+ return [self.get_item_text(i) for i in range(self.get_item_count())]
48
+
49
+ def get_last_item_text(self) -> str:
50
+ return self.items.last.inner_text()
51
+
52
+ def is_item_clickable(self, index: int) -> bool:
53
+ return self.get_item(index).locator('a').count() > 0
54
+
55
+ def is_visible(self) -> bool:
56
+ return self.breadcrumb.is_visible()
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from playwright.sync_api import Locator, Page
7
+
8
+
9
+ class Card:
10
+ """Playwright component for django_spire/card/card.html"""
11
+
12
+ def __init__(self, page: Page, card_selector: str = '.card') -> None:
13
+ self.card_selector = card_selector
14
+ self.page = page
15
+
16
+ @property
17
+ def card(self) -> Locator:
18
+ return self.page.locator(self.card_selector).first
19
+
20
+ @property
21
+ def content(self) -> Locator:
22
+ return self.card.locator('.card-body, .p-3').first
23
+
24
+ def is_visible(self) -> bool:
25
+ return self.card.is_visible()
26
+
27
+
28
+ class TitleCard(Card):
29
+ """Playwright component for django_spire/card/title_card.html"""
30
+
31
+ @property
32
+ def button(self) -> Locator:
33
+ return self.card.locator('.col-auto.d-flex').first
34
+
35
+ @property
36
+ def dropdown_content(self) -> Locator:
37
+ return self.card.locator('[x-show="card_title_dropdown"]')
38
+
39
+ @property
40
+ def title(self) -> Locator:
41
+ return self.card.locator('.card-title').first
42
+
43
+ def click_button(self) -> None:
44
+ self.button.locator('button, a').first.click()
45
+
46
+ def get_title_text(self) -> str:
47
+ return self.title.inner_text()
48
+
49
+ def has_button(self) -> bool:
50
+ return self.button.locator('button, a').count() > 0
51
+
52
+ def is_dropdown_open(self) -> bool:
53
+ return self.dropdown_content.is_visible()
54
+
55
+ def toggle_dropdown(self) -> None:
56
+ self.card.locator('[\\@click*="toggle_card_title_dropdown"]').click()
57
+
58
+
59
+ class FormCard(TitleCard):
60
+ """Playwright component for django_spire/card/form_card.html"""
61
+
62
+ @property
63
+ def description(self) -> Locator:
64
+ return self.card.locator('.mb-3').first
65
+
66
+ @property
67
+ def form(self) -> Locator:
68
+ return self.card.locator('form')
69
+
70
+ def fill_field(self, name: str, value: str) -> None:
71
+ self.form.locator(f'[name="{name}"]').fill(value)
72
+
73
+ def get_field_value(self, name: str) -> str:
74
+ return self.form.locator(f'[name="{name}"]').input_value()
75
+
76
+ def submit(self) -> None:
77
+ self.form.locator('button[type="submit"], input[type="submit"]').click()
78
+
79
+
80
+ class InfiniteScrollCard(TitleCard):
81
+ """Playwright component for django_spire/card/infinite_scroll_card.html"""
82
+
83
+ @property
84
+ def loaded_count_text(self) -> Locator:
85
+ return self.card.locator('[x-text="loaded_count"]')
86
+
87
+ @property
88
+ def scroll_container(self) -> Locator:
89
+ return self.card.locator('[x-ref="scroll_container"]')
90
+
91
+ @property
92
+ def total_count_text(self) -> Locator:
93
+ return self.card.locator('[x-text="total_count"]')
94
+
95
+ def get_loaded_count(self) -> int:
96
+ return int(self.loaded_count_text.inner_text())
97
+
98
+ def get_total_count(self) -> int:
99
+ return int(self.total_count_text.inner_text())
100
+
101
+ def scroll_to_bottom(self) -> None:
102
+ self.scroll_container.evaluate('el => el.scrollTop = el.scrollHeight')