django-spire 0.16.5__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/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/templates/django_spire/element/pagination_element.html +2 -0
- 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-0.16.5.dist-info → django_spire-0.16.6.dist-info}/METADATA +1 -1
- {django_spire-0.16.5.dist-info → django_spire-0.16.6.dist-info}/RECORD +29 -68
- django_spire/core/management/commands/spire_startapp_pkg/template/app/__init__.py +0 -0
- 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-0.16.5.dist-info → django_spire-0.16.6.dist-info}/WHEEL +0 -0
- {django_spire-0.16.5.dist-info → django_spire-0.16.6.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.16.5.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 %}
|
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}'
|
|
@@ -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()
|
|
@@ -4,25 +4,39 @@ import json
|
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
6
|
from django.contrib.sites.models import Site
|
|
7
|
-
from django.db.models import
|
|
7
|
+
from django.db.models import Prefetch
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
|
|
10
|
+
from django_spire.auth.controller.controller import AppAuthController
|
|
10
11
|
from django_spire.contrib.service import BaseDjangoModelService
|
|
11
12
|
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
|
-
from django.
|
|
16
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
16
17
|
from django_spire.knowledge.collection.models import Collection
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class CollectionTransformationService(BaseDjangoModelService['Collection']):
|
|
20
21
|
obj: Collection
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
def to_hierarchy_json(self, request: WSGIRequest) -> str:
|
|
24
|
+
user = request.user
|
|
25
|
+
|
|
26
|
+
collections = (
|
|
27
|
+
self.obj_class.objects
|
|
28
|
+
.active()
|
|
29
|
+
.select_related('parent')
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if not (
|
|
33
|
+
user.is_superuser or
|
|
34
|
+
AppAuthController('knowledge', request).can_access_all_collections()
|
|
35
|
+
):
|
|
36
|
+
collections = collections.user_has_access(user=user)
|
|
37
|
+
|
|
24
38
|
entry_queryset = (
|
|
25
|
-
|
|
39
|
+
collections.model._meta.fields_map.get('entry')
|
|
26
40
|
.related_model
|
|
27
41
|
.objects
|
|
28
42
|
.active()
|
|
@@ -33,7 +47,7 @@ class CollectionTransformationService(BaseDjangoModelService['Collection']):
|
|
|
33
47
|
)
|
|
34
48
|
|
|
35
49
|
collections = list(
|
|
36
|
-
|
|
50
|
+
collections.prefetch_related(Prefetch('entries', queryset=entry_queryset))
|
|
37
51
|
.active()
|
|
38
52
|
.order_by('order')
|
|
39
53
|
)
|
|
@@ -44,7 +58,7 @@ class CollectionTransformationService(BaseDjangoModelService['Collection']):
|
|
|
44
58
|
|
|
45
59
|
tree = []
|
|
46
60
|
for collection in collections:
|
|
47
|
-
if collection.parent_id:
|
|
61
|
+
if collection.parent_id and collection.parent_id in collection_map:
|
|
48
62
|
collection_map[collection.parent_id]['children'].append(
|
|
49
63
|
collection_map[collection.pk]
|
|
50
64
|
)
|
|
@@ -74,6 +88,14 @@ class CollectionTransformationService(BaseDjangoModelService['Collection']):
|
|
|
74
88
|
)
|
|
75
89
|
}
|
|
76
90
|
''',
|
|
91
|
+
'edit_url': f'''
|
|
92
|
+
{site}{
|
|
93
|
+
reverse(
|
|
94
|
+
'django_spire:knowledge:collection:form:update',
|
|
95
|
+
kwargs={'pk': self.obj.pk},
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
''',
|
|
77
99
|
'create_entry_url': f'''
|
|
78
100
|
{site}{
|
|
79
101
|
reverse(
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
|
|
3
3
|
from django.contrib.sites.models import Site
|
|
4
4
|
from django.conf import settings
|
|
5
|
+
from django.test import RequestFactory
|
|
5
6
|
|
|
6
7
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
7
8
|
from django_spire.knowledge.collection.models import Collection
|
|
@@ -29,9 +30,10 @@ class TestCollectionTransformationService(BaseTestCase):
|
|
|
29
30
|
self.test_collection_2 = Collection.objects.create(name='Parent A1', id=2, parent_id=1)
|
|
30
31
|
self.test_collection_3 = Collection.objects.create(name='Child A1a', id=3, parent_id=2)
|
|
31
32
|
|
|
33
|
+
request = RequestFactory().get('/')
|
|
34
|
+
request.user = self.super_user
|
|
32
35
|
family_tree = Collection.services.transformation.to_hierarchy_json(
|
|
33
|
-
|
|
34
|
-
user=self.super_user
|
|
36
|
+
request=request
|
|
35
37
|
)
|
|
36
38
|
|
|
37
39
|
for collection_json in json.loads(family_tree):
|
|
@@ -8,10 +8,11 @@ from django.http import HttpResponseRedirect
|
|
|
8
8
|
from django.urls import reverse
|
|
9
9
|
|
|
10
10
|
from django_spire.auth.controller.controller import AppAuthController
|
|
11
|
+
from django_spire.auth.group.models import AuthGroup
|
|
11
12
|
from django_spire.contrib.form.utils import show_form_errors
|
|
12
13
|
from django_spire.contrib.generic_views import portal_views
|
|
13
14
|
from django_spire.core.shortcuts import get_object_or_null_obj
|
|
14
|
-
from django_spire.knowledge.collection.models import Collection
|
|
15
|
+
from django_spire.knowledge.collection.models import Collection, CollectionGroup
|
|
15
16
|
from django_spire.knowledge.collection.forms import CollectionForm
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
@@ -30,7 +31,13 @@ def form_view(
|
|
|
30
31
|
dg.glue_query_set(
|
|
31
32
|
request,
|
|
32
33
|
unique_name='collections',
|
|
33
|
-
target=Collection.objects.active(),
|
|
34
|
+
target=Collection.objects.active().user_has_access(request.user),
|
|
35
|
+
fields=['name']
|
|
36
|
+
)
|
|
37
|
+
dg.glue_query_set(
|
|
38
|
+
request,
|
|
39
|
+
unique_name='group_query_set',
|
|
40
|
+
target=AuthGroup.objects.all(),
|
|
34
41
|
fields=['name']
|
|
35
42
|
)
|
|
36
43
|
|
|
@@ -38,7 +45,13 @@ def form_view(
|
|
|
38
45
|
form = CollectionForm(request.POST, instance=collection)
|
|
39
46
|
|
|
40
47
|
if form.is_valid():
|
|
41
|
-
|
|
48
|
+
collection, _ = collection.services.save_model_obj(**form.cleaned_data)
|
|
49
|
+
|
|
50
|
+
_ = CollectionGroup.services.factory.replace_groups(
|
|
51
|
+
request=request,
|
|
52
|
+
group_pks=dict(request.POST).get('groups'),
|
|
53
|
+
collection=collection,
|
|
54
|
+
)
|
|
42
55
|
|
|
43
56
|
return HttpResponseRedirect(
|
|
44
57
|
reverse('django_spire:knowledge:collection:page:list')
|
|
@@ -53,7 +66,10 @@ def form_view(
|
|
|
53
66
|
form=form,
|
|
54
67
|
obj=collection,
|
|
55
68
|
context_data={
|
|
56
|
-
'collection': collection
|
|
69
|
+
'collection': collection,
|
|
70
|
+
'group_ids': list(
|
|
71
|
+
collection.groups.all().values_list('auth_group_id', flat=True)
|
|
72
|
+
) if collection.id else [],
|
|
57
73
|
},
|
|
58
74
|
template='django_spire/knowledge/collection/page/form_page.html'
|
|
59
75
|
)
|
|
@@ -11,7 +11,6 @@ def django_spire_knowledge(request: WSGIRequest) -> dict[str, Any]:
|
|
|
11
11
|
|
|
12
12
|
return {
|
|
13
13
|
'collection_tree_json': Collection.services.transformation.to_hierarchy_json(
|
|
14
|
-
|
|
15
|
-
user=request.user,
|
|
14
|
+
request=request,
|
|
16
15
|
)
|
|
17
16
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-23 14:11
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('django_spire_auth_group', '0001_initial'),
|
|
11
|
+
('django_spire_knowledge', '0003_alter_collection_order_alter_entry_order_and_more'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AlterModelOptions(
|
|
16
|
+
name='collection',
|
|
17
|
+
options={'permissions': [('can_access_all_collections', 'Can Access All Collections'), ('can_change_collection_groups', 'Can Change Collection Groups')], 'verbose_name': 'Collection', 'verbose_name_plural': 'Collections'},
|
|
18
|
+
),
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='CollectionGroup',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
23
|
+
('auth_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_groups', related_query_name='collection_group', to='django_spire_auth_group.authgroup')),
|
|
24
|
+
('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', related_query_name='group', to='django_spire_knowledge.collection')),
|
|
25
|
+
],
|
|
26
|
+
),
|
|
27
|
+
]
|
|
@@ -28,27 +28,29 @@ class Entry {
|
|
|
28
28
|
|
|
29
29
|
class Collection {
|
|
30
30
|
constructor({
|
|
31
|
-
children = [],
|
|
32
|
-
create_entry_url = '',
|
|
33
|
-
delete_url = '',
|
|
34
|
-
description = '',
|
|
35
|
-
entries = [],
|
|
36
31
|
id = -1,
|
|
37
|
-
import_entry_url = '',
|
|
38
32
|
name = 'None',
|
|
33
|
+
description = '',
|
|
39
34
|
order = 0,
|
|
40
35
|
parent = null,
|
|
36
|
+
children = [],
|
|
37
|
+
entries = [],
|
|
38
|
+
delete_url = '',
|
|
39
|
+
edit_url = '',
|
|
40
|
+
create_entry_url = '',
|
|
41
|
+
import_entry_url = '',
|
|
41
42
|
}) {
|
|
42
|
-
this.children = children
|
|
43
|
-
this.create_entry_url = create_entry_url
|
|
44
|
-
this.delete_url = delete_url
|
|
45
|
-
this.description = description
|
|
46
|
-
this.entries = entries
|
|
47
43
|
this.id = id
|
|
48
|
-
this.import_entry_url = import_entry_url
|
|
49
44
|
this.name = name
|
|
45
|
+
this.description = description
|
|
50
46
|
this.order = order
|
|
51
47
|
this.parent = parent
|
|
48
|
+
this.children = children
|
|
49
|
+
this.entries = entries
|
|
50
|
+
this.delete_url = delete_url
|
|
51
|
+
this.edit_url = edit_url
|
|
52
|
+
this.create_entry_url = create_entry_url
|
|
53
|
+
this.import_entry_url = import_entry_url
|
|
52
54
|
this.show_details = false
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -107,9 +109,10 @@ class CollectionManager {
|
|
|
107
109
|
parent: new Collection({}),
|
|
108
110
|
children: this._create_tree_structure({collections: collection.children}),
|
|
109
111
|
entries: this._create_entries({entries_json: collection.entries}),
|
|
112
|
+
delete_url: collection.delete_url,
|
|
113
|
+
edit_url: collection.edit_url,
|
|
110
114
|
create_entry_url: collection.create_entry_url,
|
|
111
|
-
import_entry_url: collection.import_entry_url
|
|
112
|
-
delete_url: collection.delete_url
|
|
115
|
+
import_entry_url: collection.import_entry_url
|
|
113
116
|
})
|
|
114
117
|
|
|
115
118
|
this.collection_lookup_map.set(collection_object.id, collection_object)
|