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.
- django_spire/auth/group/forms.py +3 -4
- django_spire/auth/group/utils.py +1 -2
- django_spire/auth/group/views/form_views.py +22 -14
- django_spire/auth/group/views/page_views.py +1 -1
- django_spire/auth/mfa/utils.py +1 -1
- django_spire/auth/permissions/consts.py +2 -1
- django_spire/auth/permissions/decorators.py +1 -2
- django_spire/auth/permissions/permissions.py +1 -3
- django_spire/comment/factories.py +1 -1
- django_spire/comment/mixins.py +1 -1
- django_spire/comment/views.py +1 -1
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
- django_spire/contrib/form/confirmation_forms.py +1 -1
- django_spire/contrib/form/utils.py +26 -16
- django_spire/contrib/generic_views/modal_views.py +1 -1
- django_spire/contrib/generic_views/portal_views.py +18 -55
- django_spire/contrib/ordering/validators.py +4 -8
- django_spire/contrib/queryset/enums.py +2 -3
- django_spire/contrib/queryset/mixins.py +17 -6
- django_spire/contrib/session/controller.py +0 -1
- django_spire/core/context_processors.py +1 -1
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
- django_spire/core/middleware/maintenance.py +2 -1
- django_spire/core/middleware.py +2 -1
- django_spire/core/redirect/generic_redirect.py +1 -1
- django_spire/core/redirect/safe_redirect.py +2 -1
- django_spire/core/shortcuts.py +4 -2
- django_spire/core/table/__init__.py +0 -0
- django_spire/core/table/enums.py +18 -0
- django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
- django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
- django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
- django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
- django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
- django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
- django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
- django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
- django_spire/core/templates/django_spire/table/base.html +185 -373
- django_spire/core/templates/django_spire/table/element/footer.html +7 -15
- django_spire/core/templates/django_spire/table/element/header.html +1 -1
- django_spire/core/templates/django_spire/table/element/row.html +15 -7
- django_spire/core/templatetags/spire_core_tags.py +1 -2
- django_spire/file/fields.py +1 -1
- django_spire/file/interfaces.py +1 -1
- django_spire/file/views.py +1 -1
- django_spire/history/activity/utils.py +1 -1
- django_spire/history/mixins.py +0 -4
- django_spire/notification/app/context_data.py +3 -1
- django_spire/notification/app/views/json_views.py +1 -1
- django_spire/notification/app/views/page_views.py +2 -1
- django_spire/notification/app/views/template_views.py +2 -2
- django_spire/profiling/middleware/profiling.py +2 -2
- django_spire/profiling/panel.py +2 -2
- django_spire/testing/__init__.py +0 -0
- django_spire/testing/playwright/__init__.py +64 -0
- django_spire/testing/playwright/components/__init__.py +45 -0
- django_spire/testing/playwright/components/accordion.py +55 -0
- django_spire/testing/playwright/components/attribute_element.py +73 -0
- django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
- django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
- django_spire/testing/playwright/components/card.py +102 -0
- django_spire/testing/playwright/components/dropdown.py +87 -0
- django_spire/testing/playwright/components/infinite_scroll.py +158 -0
- django_spire/testing/playwright/components/lazy_tab.py +92 -0
- django_spire/testing/playwright/components/modal.py +101 -0
- django_spire/testing/playwright/components/navigation.py +119 -0
- django_spire/testing/playwright/components/notification_bell.py +59 -0
- django_spire/testing/playwright/components/theme_selector.py +46 -0
- django_spire/testing/playwright/components/toast.py +72 -0
- django_spire/testing/playwright/fixtures.py +54 -0
- django_spire/testing/playwright/pages/__init__.py +6 -0
- django_spire/testing/playwright/pages/base.py +24 -0
- django_spire/theme/models.py +1 -1
- django_spire/theme/tests/test_context_processor.py +0 -1
- django_spire/theme/views/json_views.py +1 -1
- django_spire/theme/views/page_views.py +1 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
- {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', {
|
|
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
|
|
18
|
+
class="align-middle {% block row_class %}{% endblock %}"
|
|
15
19
|
data-row-id="{{ row.pk }}"
|
|
16
20
|
x-data="{
|
|
17
|
-
|
|
18
|
-
table_id: null,
|
|
21
|
+
has_children: false,
|
|
19
22
|
is_checked: false,
|
|
20
23
|
is_open: false,
|
|
21
|
-
|
|
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', {
|
|
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
|
|
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
|
|
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
|
django_spire/file/fields.py
CHANGED
django_spire/file/interfaces.py
CHANGED
|
@@ -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
|
django_spire/file/views.py
CHANGED
django_spire/history/mixins.py
CHANGED
|
@@ -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)
|
|
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
from
|
|
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
|
|
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
|
|
21
|
+
from typing import Any, Callable
|
|
22
22
|
|
|
23
23
|
from django.http import HttpRequest, HttpResponse
|
|
24
24
|
|
django_spire/profiling/panel.py
CHANGED
|
@@ -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
|
|
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
|
|
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')
|