django-spire 0.16.4__py3-none-any.whl → 0.16.6__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/templates/django_spire/auth/group/navigation/accordion/permission_nav_accordion.html +2 -2
- django_spire/auth/mfa/views/page_views.py +1 -2
- django_spire/auth/templates/django_spire/auth/page/password_change_done_page.html +2 -2
- django_spire/auth/templates/django_spire/auth/page/password_change_page.html +19 -3
- django_spire/auth/views/admin_views.py +16 -0
- django_spire/consts.py +1 -1
- django_spire/core/management/commands/spire_startapp.py +0 -4
- django_spire/core/management/commands/spire_startapp_pkg/processor.py +18 -3
- django_spire/core/management/commands/spire_startapp_pkg/reporter.py +32 -30
- django_spire/core/redirect/generic_redirect.py +1 -1
- django_spire/core/static/django_spire/js/theme.js +2 -2
- django_spire/core/templates/django_spire/element/pagination_element.html +2 -0
- django_spire/core/templates/django_spire/navigation/top_navigation.html +39 -35
- django_spire/core/templates/django_spire/page/full_page.html +2 -2
- django_spire/core/templatetags/json.py +7 -4
- django_spire/knowledge/auth/controller.py +10 -0
- django_spire/knowledge/collection/admin.py +10 -3
- django_spire/knowledge/collection/models.py +23 -1
- django_spire/knowledge/collection/querysets.py +19 -1
- django_spire/knowledge/collection/services/factory_service.py +42 -0
- django_spire/knowledge/collection/services/service.py +9 -1
- django_spire/knowledge/collection/services/transformation_service.py +29 -7
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +4 -2
- django_spire/knowledge/collection/views/form_views.py +20 -4
- django_spire/knowledge/context_processors.py +1 -2
- django_spire/knowledge/migrations/0004_alter_collection_options_collectiongroup.py +27 -0
- django_spire/knowledge/static/django_spire/knowledge/collection/js/managers.js +17 -14
- django_spire/knowledge/templates/django_spire/knowledge/collection/card/context_menu_card.html +16 -10
- django_spire/knowledge/templates/django_spire/knowledge/collection/form/form.html +10 -1
- django_spire/knowledge/templates/django_spire/knowledge/collection/navigation/navigation.html +3 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/card/context_menu_card.html +1 -1
- django_spire/knowledge/templates/django_spire/knowledge/page/full_page.html +1 -1
- django_spire/theme/tests/test_filesystem.py +2 -2
- django_spire/theme/tests/{test_views.py → test_views/test_json_views.py} +14 -14
- django_spire/theme/urls/__init__.py +1 -1
- django_spire/theme/urls/json_urls.py +10 -0
- {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/METADATA +1 -1
- {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/RECORD +43 -81
- django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py +0 -7
- django_spire/core/management/commands/spire_startapp_pkg/template/app/forms.py +0 -15
- django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/bots.py +0 -19
- django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/intel.py +0 -7
- django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/prompts.py +0 -30
- django_spire/core/management/commands/spire_startapp_pkg/template/app/migrations/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/models.py +0 -52
- django_spire/core/management/commands/spire_startapp_pkg/template/app/querysets.py +0 -12
- django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/seed.py +0 -3
- django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/seeder.py +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/factory_service.py +0 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/intelligence_service.py +0 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/processor_service.py +0 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/service.py +0 -20
- django_spire/core/management/commands/spire_startapp_pkg/template/app/services/transformation_service.py +0 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/test_bots.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_models.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_factory_service.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_intelligence_service.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_processor_service.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_service.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_transformation_service.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/test_form_urls.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/test_page_urls.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/test_form_views.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/test_page_views.py +0 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py +0 -9
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py +0 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/__init__.py +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/form_views.py +0 -120
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/page_views.py +0 -45
- django_spire/theme/urls/ajax_urls.py +0 -10
- /django_spire/{core/management/commands/spire_startapp_pkg/template/app → theme/tests/test_views}/__init__.py +0 -0
- /django_spire/theme/views/{ajax_views.py → json_views.py} +0 -0
- {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/WHEEL +0 -0
- {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/top_level.txt +0 -0
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
{% block accordion_links %}
|
|
10
10
|
{% if perms.django_spire_auth_group.view_authgroup %}
|
|
11
|
-
{% url '
|
|
11
|
+
{% url 'django_spire:auth:group:page:list' as group_list_url %}
|
|
12
12
|
{% include 'django_spire/navigation/elements/nav_link.html' with link_text='Groups' link_icon='bi bi-people' link_url=group_list_url %}
|
|
13
13
|
{% endif %}
|
|
14
14
|
|
|
15
15
|
{% if perms.django_spire_auth_user.view_authuser %}
|
|
16
|
-
{% url '
|
|
16
|
+
{% url 'django_spire:auth:user:page:list' as user_list_url %}
|
|
17
17
|
{% include 'django_spire/navigation/elements/nav_link.html' with link_text='Users' link_icon='bi bi-person-badge' link_url=user_list_url %}
|
|
18
18
|
{% endif %}
|
|
19
19
|
{% endblock %}
|
|
@@ -18,7 +18,7 @@ def mfa_form_view(request):
|
|
|
18
18
|
if form.is_valid():
|
|
19
19
|
mfa_code.set_expired()
|
|
20
20
|
profile.set_mfa_grace_period()
|
|
21
|
-
return HttpResponseRedirect(reverse('home:home'))
|
|
21
|
+
return HttpResponseRedirect(reverse('home:page:home'))
|
|
22
22
|
|
|
23
23
|
if 'mfa_code' in form.errors:
|
|
24
24
|
messages.error(request, form.errors['mfa_code'][0])
|
|
@@ -33,4 +33,3 @@ def mfa_form_view(request):
|
|
|
33
33
|
context=context_data,
|
|
34
34
|
template='django_spire/auth/mfa/mfa_form.html'
|
|
35
35
|
)
|
|
36
|
-
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
{% block authentication_page_content %}
|
|
8
8
|
<h3>Password Reset</h3>
|
|
9
9
|
<div class="py-2">You have successfully changed your password!</div>
|
|
10
|
-
<a class="w-100 btn btn-app-primary bg-app-primary mt-2" href="{% url 'home:home' %}">Home</a>
|
|
11
|
-
{% endblock %}
|
|
10
|
+
<a class="w-100 btn btn-app-primary bg-app-primary mt-2" href="{% url 'home:page:home' %}">Home</a>
|
|
11
|
+
{% endblock %}
|
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
<h5 class="mb-2">Change Password</h5>
|
|
5
5
|
<div>
|
|
6
6
|
<form x-data="{
|
|
7
|
+
passwords_match: false,
|
|
7
8
|
old_password: new GlueCharField('old_password', {required: true}),
|
|
8
|
-
new_password1: new GlueCharField('new_password1', {required: true}),
|
|
9
|
-
new_password2: new GlueCharField('new_password2', {required: true})
|
|
9
|
+
new_password1: new GlueCharField('new_password1', {required: true, label: 'New Password'}),
|
|
10
|
+
new_password2: new GlueCharField('new_password2', {required: true, label: 'Confirm New Password'}),
|
|
11
|
+
|
|
12
|
+
init() {
|
|
13
|
+
$watch(() => [this.new_password1, this.new_password2], () => {
|
|
14
|
+
this.passwords_match = this.new_password1.value === this.new_password2.value
|
|
15
|
+
})
|
|
16
|
+
}
|
|
10
17
|
}" method="post" class="text-app-secondary">
|
|
11
18
|
{% csrf_token %}
|
|
12
19
|
<div class="row">
|
|
@@ -26,7 +33,16 @@
|
|
|
26
33
|
</div>
|
|
27
34
|
<div class="row mt-2">
|
|
28
35
|
<div class="col">
|
|
29
|
-
<
|
|
36
|
+
<span x-show="!passwords_match" class="text-app-danger">
|
|
37
|
+
Password confirmation does not match new password.
|
|
38
|
+
</span>
|
|
39
|
+
<button
|
|
40
|
+
type="submit"
|
|
41
|
+
class="w-100 btn btn-app-primary bg-app-primary mt-2"
|
|
42
|
+
:disabled="!passwords_match"
|
|
43
|
+
>
|
|
44
|
+
Submit
|
|
45
|
+
</button>
|
|
30
46
|
</div>
|
|
31
47
|
</div>
|
|
32
48
|
</form>
|
|
@@ -18,6 +18,10 @@ class PasswordChangeView(auth_views.PasswordChangeView):
|
|
|
18
18
|
template_name = 'django_spire/auth/page/password_change_page.html'
|
|
19
19
|
success_url = reverse_lazy('django_spire:auth:admin:password_change_done')
|
|
20
20
|
|
|
21
|
+
def form_invalid(self, form):
|
|
22
|
+
show_form_errors(self.request, form)
|
|
23
|
+
return super().form_invalid(form)
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
class PasswordChangeDone(auth_views.PasswordChangeDoneView):
|
|
23
27
|
template_name = 'django_spire/auth/page/password_change_done_page.html'
|
|
@@ -28,6 +32,10 @@ class PasswordResetView(auth_views.PasswordResetView):
|
|
|
28
32
|
success_url = reverse_lazy('django_spire:auth:admin:password_reset_done')
|
|
29
33
|
template_name = 'django_spire/auth/page/password_reset_page.html'
|
|
30
34
|
|
|
35
|
+
def form_invalid(self, form):
|
|
36
|
+
show_form_errors(self.request, form)
|
|
37
|
+
return super().form_invalid(form)
|
|
38
|
+
|
|
31
39
|
|
|
32
40
|
class PasswordResetComplete(auth_views.PasswordResetCompleteView):
|
|
33
41
|
template_name = 'django_spire/auth/page/password_reset_complete_page.html'
|
|
@@ -45,6 +53,10 @@ class PasswordResetDone(auth_views.PasswordResetDoneView):
|
|
|
45
53
|
class PasswordResetKeyForm(auth_views.PasswordResetView):
|
|
46
54
|
template_name = 'django_spire/auth/page/password_reset_key_form_page.html'
|
|
47
55
|
|
|
56
|
+
def form_invalid(self, form):
|
|
57
|
+
show_form_errors(self.request, form)
|
|
58
|
+
return super().form_invalid(form)
|
|
59
|
+
|
|
48
60
|
|
|
49
61
|
class PasswordResetKeyFormDone(auth_views.PasswordResetView):
|
|
50
62
|
template_name = 'django_spire/auth/page/password_reset_key_done_page.html'
|
|
@@ -52,3 +64,7 @@ class PasswordResetKeyFormDone(auth_views.PasswordResetView):
|
|
|
52
64
|
|
|
53
65
|
class PasswordSetForm(auth_views.PasswordResetView):
|
|
54
66
|
template_name = 'django_spire/auth/page/password_set_form_page.html'
|
|
67
|
+
|
|
68
|
+
def form_invalid(self, form):
|
|
69
|
+
show_form_errors(self.request, form)
|
|
70
|
+
return super().form_invalid(form)
|
django_spire/consts.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import django_spire
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing_extensions import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
from django.conf import settings
|
|
9
8
|
from django.core.management.base import BaseCommand, CommandError
|
|
@@ -18,9 +17,6 @@ from django_spire.core.management.commands.spire_startapp_pkg.processor import (
|
|
|
18
17
|
)
|
|
19
18
|
from django_spire.core.management.commands.spire_startapp_pkg.reporter import Reporter
|
|
20
19
|
|
|
21
|
-
if TYPE_CHECKING:
|
|
22
|
-
import argparse
|
|
23
|
-
|
|
24
20
|
|
|
25
21
|
class Command(BaseCommand):
|
|
26
22
|
help = 'Create a custom Spire app.'
|
|
@@ -24,8 +24,8 @@ class BaseTemplateProcessor:
|
|
|
24
24
|
|
|
25
25
|
updated_content = self.apply_replacement(content, replacement)
|
|
26
26
|
|
|
27
|
-
with open(path, 'w', encoding='utf-8') as
|
|
28
|
-
|
|
27
|
+
with open(path, 'w', encoding='utf-8') as handle:
|
|
28
|
+
handle.write(updated_content)
|
|
29
29
|
|
|
30
30
|
def rename_file(self, path: Path, components: list[str]) -> None:
|
|
31
31
|
replacement = generate_replacement_map(components)
|
|
@@ -55,10 +55,25 @@ class AppTemplateProcessor(BaseTemplateProcessor):
|
|
|
55
55
|
self._process_files(
|
|
56
56
|
directory,
|
|
57
57
|
components,
|
|
58
|
-
'
|
|
58
|
+
'*.template',
|
|
59
59
|
lambda path: path.is_file()
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
+
self._process_files(
|
|
63
|
+
directory,
|
|
64
|
+
components,
|
|
65
|
+
'*.py',
|
|
66
|
+
lambda path: path.is_file()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self._rename_template_files(directory)
|
|
70
|
+
|
|
71
|
+
def _rename_template_files(self, directory: Path) -> None:
|
|
72
|
+
for template_file in directory.rglob('*.py.template'):
|
|
73
|
+
new_name = template_file.name.replace('.py.template', '.py')
|
|
74
|
+
new_path = template_file.parent / new_name
|
|
75
|
+
template_file.rename(new_path)
|
|
76
|
+
|
|
62
77
|
|
|
63
78
|
class HTMLTemplateProcessor(BaseTemplateProcessor):
|
|
64
79
|
def replace_template_names(self, directory: Path, components: list[str]) -> None:
|
|
@@ -26,6 +26,15 @@ class Reporter:
|
|
|
26
26
|
|
|
27
27
|
return name
|
|
28
28
|
|
|
29
|
+
def _sort_template_items(self, path: Path) -> tuple[bool, str]:
|
|
30
|
+
return (path.is_file(), path.name.lower())
|
|
31
|
+
|
|
32
|
+
def _app_transformation(self, _index: int, component: str) -> str:
|
|
33
|
+
return component
|
|
34
|
+
|
|
35
|
+
def _html_transformation(self, index: int, component: str) -> str:
|
|
36
|
+
return 'templates' if index == 0 else component
|
|
37
|
+
|
|
29
38
|
def _report_tree_structure(
|
|
30
39
|
self,
|
|
31
40
|
title: str,
|
|
@@ -34,8 +43,11 @@ class Reporter:
|
|
|
34
43
|
registry: list[str],
|
|
35
44
|
template: Path,
|
|
36
45
|
formatter: Callable[[Path], str],
|
|
37
|
-
transformation: Callable[[int, str], str]
|
|
46
|
+
transformation: Callable[[int, str], str] | None = None,
|
|
38
47
|
) -> None:
|
|
48
|
+
if transformation is None:
|
|
49
|
+
transformation = self._app_transformation
|
|
50
|
+
|
|
39
51
|
self.command.stdout.write(title)
|
|
40
52
|
current = base
|
|
41
53
|
|
|
@@ -51,11 +63,9 @@ class Reporter:
|
|
|
51
63
|
self.command.stdout.write(f'{indent}{ICON_FOLDER_OPEN} {component}/')
|
|
52
64
|
|
|
53
65
|
if i == len(components) - 1 and app not in registry and template.exists():
|
|
54
|
-
local_formatter =
|
|
55
|
-
item
|
|
56
|
-
|
|
57
|
-
else self._apply_replacement(item.name, replacement)
|
|
58
|
-
)
|
|
66
|
+
def local_formatter(item: Path, mapping: dict[str, str] = replacement) -> str:
|
|
67
|
+
base_name = formatter(item)
|
|
68
|
+
return self._apply_replacement(base_name, mapping)
|
|
59
69
|
|
|
60
70
|
self._show_tree_from_template(template, indent + INDENTATION, local_formatter)
|
|
61
71
|
|
|
@@ -67,12 +77,7 @@ class Reporter:
|
|
|
67
77
|
) -> None:
|
|
68
78
|
ignore = {'__init__.py', '__pycache__'}
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
p.is_file(),
|
|
72
|
-
p.name.lower()
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
items = sorted(template.iterdir(), key=key)
|
|
80
|
+
items = sorted(template.iterdir(), key=self._sort_template_items)
|
|
76
81
|
|
|
77
82
|
for item in items:
|
|
78
83
|
if item.name in ignore:
|
|
@@ -88,6 +93,15 @@ class Reporter:
|
|
|
88
93
|
formatter
|
|
89
94
|
)
|
|
90
95
|
|
|
96
|
+
def _app_formatter(self, item: Path) -> str:
|
|
97
|
+
return item.name.replace('.py.template', '.py')
|
|
98
|
+
|
|
99
|
+
def _html_formatter(self, item: Path, replacement: dict[str, str]) -> str:
|
|
100
|
+
if item.is_dir():
|
|
101
|
+
return item.name
|
|
102
|
+
|
|
103
|
+
return self._apply_replacement(item.name, replacement)
|
|
104
|
+
|
|
91
105
|
def report_app_tree_structure(
|
|
92
106
|
self,
|
|
93
107
|
base: Path,
|
|
@@ -95,17 +109,14 @@ class Reporter:
|
|
|
95
109
|
registry: list[str],
|
|
96
110
|
template: Path
|
|
97
111
|
) -> None:
|
|
98
|
-
formatter = lambda item: item.name
|
|
99
|
-
transformation = lambda _, component: component
|
|
100
|
-
|
|
101
112
|
self._report_tree_structure(
|
|
102
113
|
title='\nThe following app(s) will be created:\n\n',
|
|
103
114
|
base=base,
|
|
104
115
|
components=components,
|
|
105
116
|
registry=registry,
|
|
106
117
|
template=template,
|
|
107
|
-
formatter=
|
|
108
|
-
transformation=
|
|
118
|
+
formatter=self._app_formatter,
|
|
119
|
+
transformation=self._app_transformation,
|
|
109
120
|
)
|
|
110
121
|
|
|
111
122
|
def report_html_tree_structure(
|
|
@@ -117,16 +128,8 @@ class Reporter:
|
|
|
117
128
|
) -> None:
|
|
118
129
|
replacement = generate_replacement_map(components)
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
item
|
|
122
|
-
if item.is_dir()
|
|
123
|
-
else self._apply_replacement(item.name, replacement)
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
transformation = (
|
|
127
|
-
lambda i, component:
|
|
128
|
-
'templates' if i == 0 else component
|
|
129
|
-
)
|
|
131
|
+
def html_formatter_with_replacement(item: Path) -> str:
|
|
132
|
+
return self._html_formatter(item, replacement)
|
|
130
133
|
|
|
131
134
|
self._report_tree_structure(
|
|
132
135
|
title='\nThe following template(s) will be created:\n\n',
|
|
@@ -134,8 +137,8 @@ class Reporter:
|
|
|
134
137
|
components=components,
|
|
135
138
|
registry=registry,
|
|
136
139
|
template=template,
|
|
137
|
-
formatter=
|
|
138
|
-
transformation=
|
|
140
|
+
formatter=html_formatter_with_replacement,
|
|
141
|
+
transformation=self._html_transformation,
|
|
139
142
|
)
|
|
140
143
|
|
|
141
144
|
def prompt_for_confirmation(self, message: str) -> bool:
|
|
@@ -148,7 +151,6 @@ class Reporter:
|
|
|
148
151
|
def report_installed_apps_suggestion(self, missing_components: list[str]) -> None:
|
|
149
152
|
self.command.stdout.write(self.command.style.NOTICE('\nPlease add the following to INSTALLED_APPS in settings.py:'))
|
|
150
153
|
self.command.stdout.write(f'\n {missing_components[-1]}')
|
|
151
|
-
# self.command.stdout.write('\n'.join(f'"{app}"' for app in missing_components))
|
|
152
154
|
|
|
153
155
|
def report_app_creation_success(self, app: str) -> None:
|
|
154
156
|
message = f'Successfully created app: {app}'
|
|
@@ -14,7 +14,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
14
14
|
try {
|
|
15
15
|
let response = await ajax_request(
|
|
16
16
|
'GET',
|
|
17
|
-
'/theme/
|
|
17
|
+
'/django_spire/theme/json/get_config/'
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
if (response && response.data) {
|
|
@@ -142,7 +142,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
142
142
|
async persist_to_server(value) {
|
|
143
143
|
await ajax_request(
|
|
144
144
|
'POST',
|
|
145
|
-
'/theme/
|
|
145
|
+
'/django_spire/theme/json/set_theme/',
|
|
146
146
|
{ theme: value }
|
|
147
147
|
);
|
|
148
148
|
},
|
|
@@ -23,45 +23,49 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
25
|
<div class="col-auto d-flex">
|
|
26
|
-
|
|
27
|
-
{%
|
|
28
|
-
|
|
26
|
+
{% block top_navigation_icons %}
|
|
27
|
+
{% block theme_selector %}
|
|
28
|
+
<div>
|
|
29
|
+
{% include 'django_spire/theme/element/theme_selector.html' with icon_size='fs-2' %}
|
|
30
|
+
</div>
|
|
31
|
+
{% endblock %}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
<div class="dropdown">
|
|
34
|
+
<i
|
|
35
|
+
class="bi bi-person-circle fs-2 mx-2 cursor-pointer"
|
|
36
|
+
id="dropdownMenuButton1"
|
|
37
|
+
data-bs-toggle="dropdown"
|
|
38
|
+
aria-expanded="false"
|
|
39
|
+
>
|
|
40
|
+
</i>
|
|
41
|
+
<ul class="dropdown-menu py-0 bg-app-layer-one" aria-labelledby="dropdownMenuButton1">
|
|
42
|
+
{% if request.user.is_superuser %}
|
|
43
|
+
<li class="py-0 ">
|
|
44
|
+
<a class="dropdown-item fs--1 bg-app-layer-three-hover"
|
|
45
|
+
href="{% url 'admin:index' %}" target="_blank">
|
|
46
|
+
Admin Panel
|
|
47
|
+
</a>
|
|
48
|
+
</li>
|
|
49
|
+
{% endif %}
|
|
50
|
+
<li class="py-0">
|
|
51
|
+
<a href="{% url 'django_spire:auth:admin:password_change' %}"
|
|
52
|
+
class="dropdown-item fs--1 bg-app-layer-three-hover">
|
|
53
|
+
Change Password
|
|
44
54
|
</a>
|
|
45
55
|
</li>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
<a href="{% url 'django_spire:auth:redirect:logout' %}"
|
|
55
|
-
class="dropdown-item fs--1 bg-app-layer-three-hover">
|
|
56
|
-
Logout
|
|
57
|
-
</a>
|
|
58
|
-
</li>
|
|
59
|
-
</ul>
|
|
60
|
-
</div>
|
|
56
|
+
<li class="py-0">
|
|
57
|
+
<a href="{% url 'django_spire:auth:redirect:logout' %}"
|
|
58
|
+
class="dropdown-item fs--1 bg-app-layer-three-hover">
|
|
59
|
+
Logout
|
|
60
|
+
</a>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</div>
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
<div class="col-auto d-lg-none">
|
|
66
|
+
{% include 'django_spire/navigation/mobile_navigation.html' %}
|
|
67
|
+
</div>
|
|
68
|
+
{% endblock %}
|
|
65
69
|
</div>
|
|
66
70
|
{% endblock %}
|
|
67
71
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{% endblock %}
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<div class="
|
|
11
|
+
<div class="d-flex flex-column container-fluid">
|
|
12
12
|
<div class="row sticky-top">
|
|
13
13
|
<div class="col-12 sticky-top">
|
|
14
14
|
{% block full_page_top_navigation %}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
<div class="row">
|
|
20
|
+
<div class="row flex-grow-1">
|
|
21
21
|
<div class="col-12">
|
|
22
22
|
{% block full_page_content %}
|
|
23
23
|
{% endblock %}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
1
3
|
from django import template
|
|
2
4
|
import json
|
|
3
5
|
|
|
4
6
|
register = template.Library()
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
@register.filter(name='to_json')
|
|
10
|
+
def to_json(value: dict | Sequence) -> str:
|
|
8
11
|
try:
|
|
9
|
-
return json.
|
|
12
|
+
return json.dumps(value)
|
|
10
13
|
except (TypeError, ValueError) as e:
|
|
11
|
-
return
|
|
14
|
+
return ''
|
|
@@ -2,12 +2,22 @@ from django_spire.auth.controller.controller import BaseAuthController
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class BaseKnowledgeAuthController(BaseAuthController):
|
|
5
|
+
def can_access_all_collections(self):
|
|
6
|
+
return self.request.user.has_perm(
|
|
7
|
+
'django_spire_knowledge.can_access_all_collections'
|
|
8
|
+
)
|
|
9
|
+
|
|
5
10
|
def can_add(self):
|
|
6
11
|
return self.request.user.has_perm('django_spire_knowledge.add_collection')
|
|
7
12
|
|
|
8
13
|
def can_change(self):
|
|
9
14
|
return self.request.user.has_perm('django_spire_knowledge.change_collection')
|
|
10
15
|
|
|
16
|
+
def can_change_collection_groups(self):
|
|
17
|
+
return self.request.user.has_perm(
|
|
18
|
+
'django_spire_knowledge.can_change_collection_groups'
|
|
19
|
+
)
|
|
20
|
+
|
|
11
21
|
def can_delete(self):
|
|
12
22
|
return self.request.user.has_perm('django_spire_knowledge.delete_collection')
|
|
13
23
|
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
|
-
from .models import Collection
|
|
2
|
+
from .models import Collection, CollectionGroup
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
@admin.register(Collection)
|
|
6
6
|
class CollectionAdmin(admin.ModelAdmin):
|
|
7
|
-
list_display = ['name', 'parent', 'is_deleted']
|
|
7
|
+
list_display = ['id', 'name', 'parent', 'is_deleted']
|
|
8
8
|
list_select_related = ['parent']
|
|
9
9
|
list_filter = ['is_deleted', 'is_active']
|
|
10
|
-
search_fields = ['name', 'description', 'parent__name']
|
|
10
|
+
search_fields = ['id', 'name', 'description', 'parent__name']
|
|
11
11
|
ordering = ['name']
|
|
12
12
|
autocomplete_fields = ['parent']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@admin.register(CollectionGroup)
|
|
16
|
+
class CollectionGroupAdmin(admin.ModelAdmin):
|
|
17
|
+
list_display = ['id', 'collection', 'auth_group']
|
|
18
|
+
list_select_related = ['collection', 'auth_group']
|
|
19
|
+
search_fields = ['id', 'collection__name', 'auth_group__name']
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
|
|
3
|
+
from django_spire.auth.group.models import AuthGroup
|
|
3
4
|
from django_spire.contrib.ordering.mixins import OrderingModelMixin
|
|
4
5
|
from django_spire.history.mixins import HistoryModelMixin
|
|
5
6
|
from django_spire.knowledge.collection.querysets import CollectionQuerySet
|
|
6
|
-
from django_spire.knowledge.collection.services.service import
|
|
7
|
+
from django_spire.knowledge.collection.services.service import CollectionGroupService, \
|
|
8
|
+
CollectionService
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class Collection(HistoryModelMixin, OrderingModelMixin):
|
|
@@ -29,3 +31,23 @@ class Collection(HistoryModelMixin, OrderingModelMixin):
|
|
|
29
31
|
verbose_name = 'Collection'
|
|
30
32
|
verbose_name_plural = 'Collections'
|
|
31
33
|
db_table = 'django_spire_knowledge_collection'
|
|
34
|
+
permissions = [
|
|
35
|
+
('can_access_all_collections', 'Can Access All Collections'),
|
|
36
|
+
('can_change_collection_groups', 'Can Change Collection Groups')
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CollectionGroup(models.Model):
|
|
41
|
+
collection = models.ForeignKey(
|
|
42
|
+
Collection,
|
|
43
|
+
on_delete=models.CASCADE,
|
|
44
|
+
related_name='groups',
|
|
45
|
+
related_query_name='group',
|
|
46
|
+
)
|
|
47
|
+
auth_group = models.ForeignKey(
|
|
48
|
+
AuthGroup,
|
|
49
|
+
on_delete=models.CASCADE,
|
|
50
|
+
related_name='collection_groups',
|
|
51
|
+
related_query_name='collection_group',
|
|
52
|
+
)
|
|
53
|
+
services = CollectionGroupService()
|
|
@@ -2,12 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from django.db.models import Count
|
|
5
|
+
from django.db.models import Count, Q
|
|
6
6
|
|
|
7
7
|
from django_spire.contrib.ordering.querysets import OrderingQuerySetMixin
|
|
8
8
|
from django_spire.history.querysets import HistoryQuerySet
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
+
from django_spire.auth.user.models import AuthUser
|
|
11
12
|
from django.db.models import QuerySet
|
|
12
13
|
from django_spire.knowledge.collection.models import Collection
|
|
13
14
|
|
|
@@ -27,3 +28,20 @@ class CollectionQuerySet(HistoryQuerySet, OrderingQuerySetMixin):
|
|
|
27
28
|
|
|
28
29
|
def parentless(self) -> QuerySet[Collection]:
|
|
29
30
|
return self.filter(parent_id__isnull=True)
|
|
31
|
+
|
|
32
|
+
def user_has_access(self, user: AuthUser) -> QuerySet[Collection]:
|
|
33
|
+
direct_access = self.filter(group__auth_group__user=user)
|
|
34
|
+
accessible_ids = set(direct_access.values_list('id', flat=True))
|
|
35
|
+
|
|
36
|
+
current_level_ids = accessible_ids.copy()
|
|
37
|
+
while current_level_ids:
|
|
38
|
+
next_level = self.filter(parent_id__in=current_level_ids)
|
|
39
|
+
new_ids = set(next_level.values_list('id', flat=True)) - accessible_ids
|
|
40
|
+
|
|
41
|
+
if not new_ids:
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
accessible_ids.update(new_ids)
|
|
45
|
+
current_level_ids = new_ids
|
|
46
|
+
|
|
47
|
+
return self.filter(id__in=accessible_ids).distinct()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
6
|
+
|
|
7
|
+
from django_spire.auth.controller.controller import AppAuthController
|
|
8
|
+
from django_spire.contrib.service import BaseDjangoModelService
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from django_spire.knowledge.collection.models import Collection, CollectionGroup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CollectionGroupFactoryService(BaseDjangoModelService['CollectionGroup']):
|
|
15
|
+
obj: CollectionGroup
|
|
16
|
+
|
|
17
|
+
def replace_groups(
|
|
18
|
+
self,
|
|
19
|
+
request: WSGIRequest,
|
|
20
|
+
group_pks: list[int] | None,
|
|
21
|
+
collection: Collection
|
|
22
|
+
) -> list[CollectionGroup]:
|
|
23
|
+
if not AppAuthController('knowledge', request).can_change_collection_groups():
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
if group_pks is None:
|
|
27
|
+
return collection.groups.all().delete()
|
|
28
|
+
|
|
29
|
+
old_collection_groups = list(collection.groups.all())
|
|
30
|
+
|
|
31
|
+
new_collection_groups = []
|
|
32
|
+
for group_pk in group_pks:
|
|
33
|
+
new_collection_groups.append(
|
|
34
|
+
self.obj_class(auth_group_id=group_pk, collection=collection)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.obj_class.objects.bulk_create(new_collection_groups)
|
|
38
|
+
|
|
39
|
+
for collection in old_collection_groups:
|
|
40
|
+
collection.delete()
|
|
41
|
+
|
|
42
|
+
return new_collection_groups
|
|
@@ -4,6 +4,8 @@ from django_spire.contrib.service import BaseDjangoModelService
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
+
from django_spire.knowledge.collection.services.factory_service import \
|
|
8
|
+
CollectionGroupFactoryService
|
|
7
9
|
from django_spire.knowledge.collection.services.ordering_service import \
|
|
8
10
|
CollectionOrderingService
|
|
9
11
|
from django_spire.knowledge.collection.services.processor_service import \
|
|
@@ -12,7 +14,7 @@ from django_spire.knowledge.collection.services.transformation_service import \
|
|
|
12
14
|
CollectionTransformationService
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
|
-
from django_spire.knowledge.collection.models import Collection
|
|
17
|
+
from django_spire.knowledge.collection.models import Collection, CollectionGroup
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class CollectionService(BaseDjangoModelService['Collection']):
|
|
@@ -40,3 +42,9 @@ class CollectionService(BaseDjangoModelService['Collection']):
|
|
|
40
42
|
)
|
|
41
43
|
|
|
42
44
|
return self.obj, created
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CollectionGroupService(BaseDjangoModelService['CollectionGroup']):
|
|
48
|
+
obj: CollectionGroup
|
|
49
|
+
|
|
50
|
+
factory = CollectionGroupFactoryService()
|