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.
Files changed (84) hide show
  1. django_spire/auth/group/templates/django_spire/auth/group/navigation/accordion/permission_nav_accordion.html +2 -2
  2. django_spire/auth/mfa/views/page_views.py +1 -2
  3. django_spire/auth/templates/django_spire/auth/page/password_change_done_page.html +2 -2
  4. django_spire/auth/templates/django_spire/auth/page/password_change_page.html +19 -3
  5. django_spire/auth/views/admin_views.py +16 -0
  6. django_spire/consts.py +1 -1
  7. django_spire/core/management/commands/spire_startapp.py +0 -4
  8. django_spire/core/management/commands/spire_startapp_pkg/processor.py +18 -3
  9. django_spire/core/management/commands/spire_startapp_pkg/reporter.py +32 -30
  10. django_spire/core/redirect/generic_redirect.py +1 -1
  11. django_spire/core/static/django_spire/js/theme.js +2 -2
  12. django_spire/core/templates/django_spire/element/pagination_element.html +2 -0
  13. django_spire/core/templates/django_spire/navigation/top_navigation.html +39 -35
  14. django_spire/core/templates/django_spire/page/full_page.html +2 -2
  15. django_spire/core/templatetags/json.py +7 -4
  16. django_spire/knowledge/auth/controller.py +10 -0
  17. django_spire/knowledge/collection/admin.py +10 -3
  18. django_spire/knowledge/collection/models.py +23 -1
  19. django_spire/knowledge/collection/querysets.py +19 -1
  20. django_spire/knowledge/collection/services/factory_service.py +42 -0
  21. django_spire/knowledge/collection/services/service.py +9 -1
  22. django_spire/knowledge/collection/services/transformation_service.py +29 -7
  23. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +4 -2
  24. django_spire/knowledge/collection/views/form_views.py +20 -4
  25. django_spire/knowledge/context_processors.py +1 -2
  26. django_spire/knowledge/migrations/0004_alter_collection_options_collectiongroup.py +27 -0
  27. django_spire/knowledge/static/django_spire/knowledge/collection/js/managers.js +17 -14
  28. django_spire/knowledge/templates/django_spire/knowledge/collection/card/context_menu_card.html +16 -10
  29. django_spire/knowledge/templates/django_spire/knowledge/collection/form/form.html +10 -1
  30. django_spire/knowledge/templates/django_spire/knowledge/collection/navigation/navigation.html +3 -0
  31. django_spire/knowledge/templates/django_spire/knowledge/entry/card/context_menu_card.html +1 -1
  32. django_spire/knowledge/templates/django_spire/knowledge/page/full_page.html +1 -1
  33. django_spire/theme/tests/test_filesystem.py +2 -2
  34. django_spire/theme/tests/{test_views.py → test_views/test_json_views.py} +14 -14
  35. django_spire/theme/urls/__init__.py +1 -1
  36. django_spire/theme/urls/json_urls.py +10 -0
  37. {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/METADATA +1 -1
  38. {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/RECORD +43 -81
  39. django_spire/core/management/commands/spire_startapp_pkg/template/app/apps.py +0 -7
  40. django_spire/core/management/commands/spire_startapp_pkg/template/app/forms.py +0 -15
  41. django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/__init__.py +0 -0
  42. django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/bots.py +0 -19
  43. django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/intel.py +0 -7
  44. django_spire/core/management/commands/spire_startapp_pkg/template/app/intelligence/prompts.py +0 -30
  45. django_spire/core/management/commands/spire_startapp_pkg/template/app/migrations/__init__.py +0 -0
  46. django_spire/core/management/commands/spire_startapp_pkg/template/app/models.py +0 -52
  47. django_spire/core/management/commands/spire_startapp_pkg/template/app/querysets.py +0 -12
  48. django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/__init__.py +0 -0
  49. django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/seed.py +0 -3
  50. django_spire/core/management/commands/spire_startapp_pkg/template/app/seeding/seeder.py +0 -13
  51. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/__init__.py +0 -0
  52. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/factory_service.py +0 -11
  53. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/intelligence_service.py +0 -11
  54. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/processor_service.py +0 -11
  55. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/service.py +0 -20
  56. django_spire/core/management/commands/spire_startapp_pkg/template/app/services/transformation_service.py +0 -11
  57. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/__init__.py +0 -0
  58. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/__init__.py +0 -0
  59. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/test_bots.py +0 -6
  60. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_models.py +0 -6
  61. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/__init__.py +0 -0
  62. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_factory_service.py +0 -6
  63. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_intelligence_service.py +0 -6
  64. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_processor_service.py +0 -6
  65. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_service.py +0 -6
  66. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_services/test_transformation_service.py +0 -6
  67. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/__init__.py +0 -0
  68. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/test_form_urls.py +0 -6
  69. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_urls/test_page_urls.py +0 -6
  70. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/__init__.py +0 -0
  71. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/test_form_views.py +0 -6
  72. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_views/test_page_views.py +0 -6
  73. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py +0 -9
  74. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py +0 -13
  75. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py +0 -11
  76. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/__init__.py +0 -0
  77. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/form_views.py +0 -120
  78. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/page_views.py +0 -45
  79. django_spire/theme/urls/ajax_urls.py +0 -10
  80. /django_spire/{core/management/commands/spire_startapp_pkg/template/app → theme/tests/test_views}/__init__.py +0 -0
  81. /django_spire/theme/views/{ajax_views.py → json_views.py} +0 -0
  82. {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/WHEEL +0 -0
  83. {django_spire-0.16.4.dist-info → django_spire-0.16.6.dist-info}/licenses/LICENSE.md +0 -0
  84. {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 'django_spire_permission:list' as group_list_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 'django_spire_permission:user_list' as user_list_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
- <button type="submit" class="w-100 btn btn-app-primary bg-app-primary mt-2">Submit</button>
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
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.16.4'
1
+ __VERSION__ = '0.16.6'
2
2
 
3
3
 
4
4
  AI_CHAT_WORKFLOW_SENDER_SETTINGS_NAME = 'AI_CHAT_WORKFLOW_NAME'
@@ -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 file:
28
- file.write(updated_content)
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] = lambda _, component: component,
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 = lambda item: (
55
- item.name
56
- if item.is_dir()
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
- key = lambda p: (
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=formatter,
108
- transformation=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
- formatter = lambda item: (
121
- item.name
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=formatter,
138
- transformation=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}'
@@ -35,4 +35,4 @@ def reverse_generic_relation(content_object: Any, **kwargs) -> HttpResponse | No
35
35
  if url_path is not None:
36
36
  return reverse(url_path, kwargs=kwargs)
37
37
 
38
- return HttpResponse('home:home')
38
+ return HttpResponse('home:page:home')
@@ -14,7 +14,7 @@ document.addEventListener('alpine:init', () => {
14
14
  try {
15
15
  let response = await ajax_request(
16
16
  'GET',
17
- '/theme/ajax/get_config/'
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/ajax/set_theme/',
145
+ '/django_spire/theme/json/set_theme/',
146
146
  { theme: value }
147
147
  );
148
148
  },
@@ -1,3 +1,5 @@
1
+ {% load pagination_tags %}
2
+
1
3
  {% include 'django_spire/element/divider_element.html' %}
2
4
  <div class="row justify-content-between pb-1">
3
5
  <div class="col-auto ps-4">
@@ -23,45 +23,49 @@
23
23
  </div>
24
24
 
25
25
  <div class="col-auto d-flex">
26
- <div>
27
- {% include 'django_spire/theme/element/theme_selector.html' with icon_size='fs-2' %}
28
- </div>
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
- <div class="dropdown">
31
- <i
32
- class="bi bi-person-circle fs-2 mx-2 cursor-pointer"
33
- id="dropdownMenuButton1"
34
- data-bs-toggle="dropdown"
35
- aria-expanded="false"
36
- >
37
- </i>
38
- <ul class="dropdown-menu py-0 bg-app-layer-one" aria-labelledby="dropdownMenuButton1">
39
- {% if request.user.is_superuser %}
40
- <li class="py-0 ">
41
- <a class="dropdown-item fs--1 bg-app-layer-three-hover"
42
- href="{% url 'admin:index' %}" target="_blank">
43
- Admin Panel
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
- {% endif %}
47
- <li class="py-0">
48
- <a href="{% url 'django_spire:auth:admin:password_change' %}"
49
- class="dropdown-item fs--1 bg-app-layer-three-hover">
50
- Change Password
51
- </a>
52
- </li>
53
- <li class="py-0">
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
- <div class="col-auto d-lg-none">
63
- {% include 'django_spire/navigation/mobile_navigation.html' %}
64
- </div>
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="full-page-content container-fluid">
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
- @register.filter(name='json_loads')
7
- def json_loads(value):
8
+
9
+ @register.filter(name='to_json')
10
+ def to_json(value: dict | Sequence) -> str:
8
11
  try:
9
- return json.loads(value)
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 CollectionService
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()