django-spire 0.16.12__py3-none-any.whl → 0.17.0__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 (170) hide show
  1. django_spire/ai/admin.py +3 -1
  2. django_spire/ai/apps.py +2 -0
  3. django_spire/ai/chat/admin.py +15 -9
  4. django_spire/ai/chat/apps.py +4 -1
  5. django_spire/ai/chat/auth/controller.py +3 -1
  6. django_spire/ai/chat/choices.py +2 -0
  7. django_spire/ai/chat/intelligence/maps/intent_llm_map.py +8 -5
  8. django_spire/ai/chat/intelligence/prompts.py +4 -2
  9. django_spire/ai/chat/intelligence/workflows/chat_workflow.py +27 -28
  10. django_spire/ai/chat/message_intel.py +7 -4
  11. django_spire/ai/chat/models.py +8 -9
  12. django_spire/ai/chat/querysets.py +3 -1
  13. django_spire/ai/chat/responses.py +19 -10
  14. django_spire/ai/chat/tools.py +20 -15
  15. django_spire/ai/chat/urls/message_urls.py +2 -1
  16. django_spire/ai/chat/urls/page_urls.py +1 -0
  17. django_spire/ai/chat/views/message_request_views.py +2 -0
  18. django_spire/ai/chat/views/message_response_views.py +4 -4
  19. django_spire/ai/chat/views/message_views.py +2 -0
  20. django_spire/ai/chat/views/page_views.py +7 -2
  21. django_spire/ai/chat/views/template_views.py +2 -0
  22. django_spire/ai/decorators.py +13 -7
  23. django_spire/ai/mixins.py +4 -2
  24. django_spire/ai/models.py +7 -2
  25. django_spire/ai/prompt/bots.py +14 -32
  26. django_spire/ai/prompt/intel.py +1 -1
  27. django_spire/ai/prompt/prompts.py +7 -1
  28. django_spire/ai/prompt/system/bots.py +42 -75
  29. django_spire/ai/prompt/system/intel.py +5 -4
  30. django_spire/ai/prompt/system/prompts.py +5 -1
  31. django_spire/ai/prompt/system/system_prompt_cli.py +15 -9
  32. django_spire/ai/prompt/tests/test_bots.py +14 -11
  33. django_spire/ai/prompt/text_to_prompt_cli.py +5 -2
  34. django_spire/ai/prompt/tuning/bot_tuning_cli.py +14 -13
  35. django_spire/ai/prompt/tuning/bots.py +68 -116
  36. django_spire/ai/prompt/tuning/intel.py +1 -1
  37. django_spire/ai/prompt/tuning/mixins.py +2 -0
  38. django_spire/ai/prompt/tuning/prompt_tuning_cli.py +8 -8
  39. django_spire/ai/prompt/tuning/prompts.py +4 -2
  40. django_spire/ai/sms/admin.py +3 -1
  41. django_spire/ai/sms/apps.py +2 -0
  42. django_spire/ai/sms/decorators.py +2 -0
  43. django_spire/ai/sms/intel.py +4 -2
  44. django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
  45. django_spire/ai/sms/models.py +16 -14
  46. django_spire/ai/sms/querysets.py +4 -1
  47. django_spire/ai/sms/tools.py +18 -16
  48. django_spire/ai/sms/urls.py +1 -1
  49. django_spire/ai/sms/views.py +2 -0
  50. django_spire/ai/tests/test_ai.py +3 -5
  51. django_spire/ai/urls.py +1 -0
  52. django_spire/consts.py +1 -1
  53. django_spire/contrib/seeding/field/django/seeder.py +6 -4
  54. django_spire/contrib/seeding/field/llm.py +1 -2
  55. django_spire/contrib/seeding/intelligence/bots/field_seeding_bots.py +7 -8
  56. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +15 -16
  57. django_spire/contrib/seeding/intelligence/intel.py +1 -1
  58. django_spire/contrib/seeding/intelligence/prompts/factory.py +5 -7
  59. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +3 -5
  60. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +1 -2
  61. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +3 -5
  62. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +1 -1
  63. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +3 -3
  64. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +1 -1
  65. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +2 -4
  66. django_spire/contrib/seeding/management/commands/seeding.py +5 -2
  67. django_spire/contrib/seeding/model/base.py +12 -10
  68. django_spire/contrib/seeding/model/django/seeder.py +13 -10
  69. django_spire/contrib/seeding/tests/test_seeding.py +1 -1
  70. django_spire/core/management/commands/spire_startapp.py +84 -46
  71. django_spire/core/management/commands/spire_startapp_pkg/__init__.py +60 -0
  72. django_spire/core/management/commands/spire_startapp_pkg/builder.py +91 -0
  73. django_spire/core/management/commands/spire_startapp_pkg/config.py +115 -0
  74. django_spire/core/management/commands/spire_startapp_pkg/filesystem.py +125 -0
  75. django_spire/core/management/commands/spire_startapp_pkg/generator.py +167 -0
  76. django_spire/core/management/commands/spire_startapp_pkg/maps.py +783 -25
  77. django_spire/core/management/commands/spire_startapp_pkg/permissions.py +147 -0
  78. django_spire/core/management/commands/spire_startapp_pkg/processor.py +144 -57
  79. django_spire/core/management/commands/spire_startapp_pkg/registry.py +89 -0
  80. django_spire/core/management/commands/spire_startapp_pkg/reporter.py +245 -108
  81. django_spire/core/management/commands/spire_startapp_pkg/resolver.py +86 -0
  82. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +252 -0
  83. django_spire/core/management/commands/spire_startapp_pkg/validator.py +96 -0
  84. django_spire/core/middleware/__init__.py +1 -2
  85. django_spire/knowledge/admin.py +2 -0
  86. django_spire/knowledge/apps.py +2 -0
  87. django_spire/knowledge/auth/controller.py +2 -0
  88. django_spire/knowledge/collection/admin.py +3 -0
  89. django_spire/knowledge/collection/forms.py +2 -0
  90. django_spire/knowledge/collection/models.py +4 -0
  91. django_spire/knowledge/collection/querysets.py +3 -3
  92. django_spire/knowledge/collection/seeding/seed.py +2 -0
  93. django_spire/knowledge/collection/tests/factories.py +2 -0
  94. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +2 -0
  95. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +2 -0
  96. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +2 -0
  97. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +2 -0
  98. django_spire/knowledge/collection/views/json_views.py +2 -0
  99. django_spire/knowledge/collection/views/page_views.py +2 -0
  100. django_spire/knowledge/context_processors.py +2 -0
  101. django_spire/knowledge/entry/services/tool_service.py +1 -0
  102. django_spire/knowledge/entry/services/transformation_services.py +0 -1
  103. django_spire/knowledge/entry/tests/factories.py +3 -0
  104. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +2 -0
  105. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +2 -0
  106. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +2 -0
  107. django_spire/knowledge/entry/urls/form_urls.py +1 -0
  108. django_spire/knowledge/entry/urls/json_urls.py +1 -0
  109. django_spire/knowledge/entry/urls/page_urls.py +1 -0
  110. django_spire/knowledge/entry/urls/template_urls.py +1 -0
  111. django_spire/knowledge/entry/version/admin.py +2 -0
  112. django_spire/knowledge/entry/version/block/admin.py +2 -0
  113. django_spire/knowledge/entry/version/block/blocks/heading_block.py +2 -0
  114. django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +2 -0
  115. django_spire/knowledge/entry/version/block/blocks/text_block.py +2 -0
  116. django_spire/knowledge/entry/version/block/maps.py +2 -0
  117. django_spire/knowledge/entry/version/block/models.py +2 -0
  118. django_spire/knowledge/entry/version/block/tests/factories.py +2 -0
  119. django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +2 -0
  120. django_spire/knowledge/entry/version/block/views/json_views.py +2 -0
  121. django_spire/knowledge/entry/version/intelligence/bots/markdown_format_llm_bot.py +12 -9
  122. django_spire/knowledge/entry/version/maps.py +2 -0
  123. django_spire/knowledge/entry/version/models.py +2 -0
  124. django_spire/knowledge/entry/version/querysets.py +2 -0
  125. django_spire/knowledge/entry/version/seeding/seeder.py +1 -0
  126. django_spire/knowledge/entry/version/tests/factories.py +2 -0
  127. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +3 -0
  128. django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +2 -0
  129. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +2 -0
  130. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +2 -0
  131. django_spire/knowledge/entry/version/urls/form_urls.py +1 -0
  132. django_spire/knowledge/entry/version/urls/json_urls.py +1 -0
  133. django_spire/knowledge/entry/version/urls/page_urls.py +1 -0
  134. django_spire/knowledge/entry/version/urls/redirect_urls.py +1 -0
  135. django_spire/knowledge/entry/version/views/form_views.py +2 -0
  136. django_spire/knowledge/entry/version/views/json_views.py +2 -0
  137. django_spire/knowledge/entry/version/views/page_views.py +2 -0
  138. django_spire/knowledge/entry/version/views/redirect_views.py +2 -0
  139. django_spire/knowledge/exceptions.py +2 -0
  140. django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +5 -16
  141. django_spire/knowledge/intelligence/intel/collection_intel.py +3 -1
  142. django_spire/knowledge/intelligence/intel/entry_intel.py +3 -3
  143. django_spire/knowledge/intelligence/intel/message_intel.py +2 -0
  144. django_spire/knowledge/intelligence/maps/collection_map.py +9 -10
  145. django_spire/knowledge/intelligence/maps/entry_map.py +8 -9
  146. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +8 -6
  147. django_spire/knowledge/models.py +2 -0
  148. django_spire/knowledge/seeding/seed.py +2 -0
  149. django_spire/knowledge/templatetags/spire_knowledge_tags.py +3 -0
  150. django_spire/knowledge/urls/__init__.py +1 -0
  151. django_spire/profiling/__init__.py +13 -0
  152. django_spire/profiling/middleware/__init__.py +6 -0
  153. django_spire/{core → profiling}/middleware/profiling.py +63 -58
  154. django_spire/profiling/panel.py +345 -0
  155. django_spire/profiling/templates/panel.html +166 -0
  156. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/METADATA +5 -4
  157. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/RECORD +160 -157
  158. django_spire/core/management/commands/spire_startapp_pkg/constants.py +0 -4
  159. django_spire/core/management/commands/spire_startapp_pkg/manager.py +0 -176
  160. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html +0 -24
  161. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html +0 -9
  162. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html +0 -18
  163. django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html +0 -22
  164. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html +0 -24
  165. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html +0 -13
  166. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html +0 -13
  167. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html +0 -9
  168. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/WHEEL +0 -0
  169. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from typing_extensions import TYPE_CHECKING
6
+
7
+ from django.core.management.base import CommandError
8
+
9
+ if TYPE_CHECKING:
10
+ from django_spire.core.management.commands.spire_startapp_pkg.reporter import Reporter
11
+ from django_spire.core.management.commands.spire_startapp_pkg.validator import AppValidator
12
+
13
+
14
+ class UserInputCollector:
15
+ """
16
+ Collects user input for Django app creation through an interactive wizard.
17
+
18
+ This class guides users through a step-by-step process to gather all
19
+ necessary configuration for creating a new Django app.
20
+ """
21
+
22
+ def __init__(self, reporter: Reporter, validator: AppValidator):
23
+ """
24
+ Initializes the collector with a reporter and validator.
25
+
26
+ :param reporter: Reporter instance for displaying prompts and messages.
27
+ :param validator: Validator for checking user input validity.
28
+ """
29
+
30
+ self.reporter = reporter
31
+ self.validator = validator
32
+
33
+ def collect_all_inputs(self) -> dict[str, str]:
34
+ """
35
+ Collects all required user inputs for app creation.
36
+
37
+ Guides the user through an 8-step wizard to gather app path, names,
38
+ labels, and configuration options.
39
+
40
+ :return: Dictionary containing all collected user inputs.
41
+ """
42
+
43
+ self.reporter.write('\n[App Creation Wizard]\n\n', self.reporter.style_success)
44
+
45
+ app_path = self._collect_app_path()
46
+ components = app_path.split('.')
47
+
48
+ app_name = self._collect_app_name(components)
49
+ app_label = self._collect_app_label(components, app_name)
50
+ model_name = self._collect_model_name(app_name)
51
+ model_name_plural = self._collect_model_name_plural(model_name)
52
+ db_table_name = self._collect_db_table_name(app_label)
53
+ model_permission_path = self._collect_model_permission_path(app_path, model_name)
54
+
55
+ permission_data = self._collect_permission_inheritance(components)
56
+
57
+ verbose_name, verbose_name_plural = self._derive_verbose_names(model_name, model_name_plural)
58
+
59
+ return {
60
+ 'app_path': app_path,
61
+ 'app_name': app_name,
62
+ 'model_name': model_name,
63
+ 'model_name_plural': model_name_plural,
64
+ 'app_label': app_label,
65
+ 'db_table_name': db_table_name,
66
+ 'model_permission_path': model_permission_path,
67
+ 'verbose_name': verbose_name,
68
+ 'verbose_name_plural': verbose_name_plural,
69
+ **permission_data,
70
+ }
71
+
72
+ def _collect_app_label(self, components: list[str], app_name: str) -> str:
73
+ """
74
+ Prompts the user for the Django app label.
75
+
76
+ :param components: List of app path components.
77
+ :param app_name: Name of the app.
78
+ :return: User-provided or default app label.
79
+ """
80
+
81
+ parent_parts = components[1:-1] if len(components) > 1 else []
82
+ default = '_'.join(parent_parts).lower() + '_' + app_name.lower() if parent_parts else app_name.lower()
83
+ return self._collect_input('Enter the app label', default, '3/8')
84
+
85
+ def _collect_app_name(self, components: list[str]) -> str:
86
+ """
87
+ Prompts the user for the app name.
88
+
89
+ :param components: List of app path components.
90
+ :return: User-provided or default app name.
91
+ """
92
+
93
+ default = components[-1]
94
+ return self._collect_input('Enter the app name', default, '2/8')
95
+
96
+ def _collect_app_path(self) -> str:
97
+ """
98
+ Prompts the user for the app path and validates it.
99
+
100
+ :return: Validated app path in dot notation.
101
+ :raises CommandError: If the app path is empty or invalid.
102
+ """
103
+
104
+ app_path = self._collect_simple_input('Enter the app path (e.g., "app.human_resource.employee.skill")', '1/8')
105
+
106
+ if not app_path:
107
+ self.reporter.write('\n', self.reporter.style_notice)
108
+
109
+ message = 'The app path is required'
110
+ raise CommandError(message)
111
+
112
+ components = app_path.split('.')
113
+ self.validator.validate_app_path(components)
114
+
115
+ return app_path
116
+
117
+ def _collect_db_table_name(self, app_label: str) -> str:
118
+ """
119
+ Prompts the user for the database table name.
120
+
121
+ :param app_label: App label to use as default.
122
+ :return: User-provided or default database table name.
123
+ """
124
+
125
+ return self._collect_input('Enter the database table name', app_label, '6/8')
126
+
127
+ def _collect_input(self, prompt: str, default: str, step_number: str) -> str:
128
+ """
129
+ Prompts the user for input with a default value.
130
+
131
+ :param prompt: Prompt message to display.
132
+ :param default: Default value if user presses Enter.
133
+ :param step_number: Step number in the wizard (e.g., '1/8').
134
+ :return: User-provided input or default value.
135
+ """
136
+
137
+ self.reporter.write(f'\n[{step_number}]: {prompt} (default: "{default}")', self.reporter.style_notice)
138
+ user_input = input('Press Enter to use default or type a custom value: ').strip()
139
+ return user_input if user_input else default
140
+
141
+ def _collect_model_name(self, app_name: str) -> str:
142
+ """
143
+ Prompts the user for the model class name.
144
+
145
+ :param app_name: App name to derive default from.
146
+ :return: User-provided or default model name in TitleCase.
147
+ """
148
+
149
+ default = ''.join(word.title() for word in app_name.split('_'))
150
+ return self._collect_input('Enter the model name', default, '4/8')
151
+
152
+ def _collect_model_name_plural(self, model_name: str) -> str:
153
+ """
154
+ Prompts the user for the plural form of the model name.
155
+
156
+ :param model_name: Singular model name.
157
+ :return: User-provided or default plural model name.
158
+ """
159
+
160
+ default = model_name + 's'
161
+ return self._collect_input('Enter the model name plural', default, '5/8')
162
+
163
+ def _collect_model_permission_path(self, app_path: str, model_name: str) -> str:
164
+ """
165
+ Prompts the user for the model permission path.
166
+
167
+ :param app_path: Dot-separated app path.
168
+ :param model_name: Model class name.
169
+ :return: User-provided or default model permission path.
170
+ """
171
+
172
+ default = f'{app_path}.models.{model_name}'
173
+ return self._collect_input('Enter the model permission path', default, '7/8')
174
+
175
+ def _collect_permission_inheritance(self, components: list[str]) -> dict[str, str]:
176
+ """
177
+ Collects permission inheritance configuration if applicable.
178
+
179
+ Prompts the user about inheriting permissions from parent apps
180
+ and collects necessary parent model information.
181
+
182
+ :param components: List of app path components.
183
+ :return: Dictionary containing permission inheritance settings.
184
+ """
185
+
186
+ if len(components) <= 2:
187
+ return {'inherit_permissions': False, 'parent_permission_prefix': '', 'parent_model_instance_name': ''}
188
+
189
+ if not self._should_inherit_permissions():
190
+ return {'inherit_permissions': False, 'parent_permission_prefix': '', 'parent_model_instance_name': ''}
191
+
192
+ parent_parts = components[1:-1]
193
+ parent_name = components[-2]
194
+ parent_model_class = ''.join(word.title() for word in parent_name.split('_'))
195
+
196
+ self.reporter.write('\n[Permission Inheritance Configuration]\n', self.reporter.style_notice)
197
+
198
+ return {
199
+ 'inherit_permissions': True,
200
+ 'parent_permission_prefix': self._collect_input(
201
+ 'Enter the parent permission prefix',
202
+ '_'.join(parent_parts).lower(),
203
+ '1/3'
204
+ ),
205
+ 'parent_model_instance_name': self._collect_input(
206
+ 'Enter the parent model instance name',
207
+ parent_name.lower(),
208
+ '2/3'
209
+ ),
210
+ 'parent_model_path': self._collect_input(
211
+ 'Enter the parent model path',
212
+ '.'.join(components[:-1]) + f'.models.{parent_model_class}',
213
+ '3/3'
214
+ ),
215
+ }
216
+
217
+ def _collect_simple_input(self, prompt: str, step_number: str) -> str:
218
+ """
219
+ Prompts the user for simple input without a default value.
220
+
221
+ :param prompt: Prompt message to display.
222
+ :param step_number: Step number in the wizard.
223
+ :return: User-provided input.
224
+ """
225
+
226
+ return input(f'[{step_number}]: {prompt}: ').strip()
227
+
228
+ def _derive_verbose_names(self, model_name: str, model_name_plural: str) -> tuple[str, str]:
229
+ """
230
+ Derives human-readable verbose names from model names.
231
+
232
+ Converts CamelCase model names to space-separated words.
233
+
234
+ :param model_name: Singular model name in CamelCase.
235
+ :param model_name_plural: Plural model name in CamelCase.
236
+ :return: Tuple of (verbose_name, verbose_name_plural).
237
+ """
238
+
239
+ verbose_name = re.sub(r'(?<!^)(?=[A-Z])', ' ', model_name)
240
+ verbose_name_plural = re.sub(r'(?<!^)(?=[A-Z])', ' ', model_name_plural)
241
+ return verbose_name, verbose_name_plural
242
+
243
+ def _should_inherit_permissions(self) -> bool:
244
+ """
245
+ Prompts the user to confirm permission inheritance.
246
+
247
+ :return: True if user wants to inherit permissions, False otherwise.
248
+ """
249
+
250
+ self.reporter.write('\n[8/8]: Do you want this app to inherit permissions from its parent? (y/n)', self.reporter.style_notice)
251
+ user_input = input('Default is "n": ').strip().lower()
252
+ return user_input == 'y'
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from django.core.management.base import CommandError
6
+
7
+ if TYPE_CHECKING:
8
+ from django_spire.core.management.commands.spire_startapp_pkg.filesystem import FileSystemInterface
9
+ from django_spire.core.management.commands.spire_startapp_pkg.registry import AppRegistryInterface
10
+ from django_spire.core.management.commands.spire_startapp_pkg.reporter import ReporterInterface
11
+ from django_spire.core.management.commands.spire_startapp_pkg.resolver import PathResolverInterface
12
+
13
+
14
+ class AppValidator:
15
+ """
16
+ Validates Django app paths and configurations.
17
+
18
+ This class performs validation checks to ensure app paths are properly
19
+ formatted, don't conflict with existing apps, and use valid root apps.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ reporter: ReporterInterface,
25
+ registry: AppRegistryInterface,
26
+ path_resolver: PathResolverInterface,
27
+ filesystem: FileSystemInterface
28
+ ):
29
+ """
30
+ Initializes the validator with required dependencies.
31
+
32
+ :param reporter: Reporter for displaying error messages.
33
+ :param registry: Registry for checking installed apps.
34
+ :param path_resolver: Path resolver for determining file locations.
35
+ :param filesystem: File system interface for checking file existence.
36
+ """
37
+
38
+ self._filesystem = filesystem
39
+ self._path_resolver = path_resolver
40
+ self._registry = registry
41
+ self._reporter = reporter
42
+
43
+ def validate_app_format(self, app_path: str) -> None:
44
+ """
45
+ Validates that an app path uses dot notation.
46
+
47
+ :param app_path: App path to validate.
48
+ :raises CommandError: If the app path doesn't contain dots.
49
+ """
50
+
51
+ if '.' not in app_path:
52
+ self._reporter.write('\n', self._reporter.style_notice)
53
+
54
+ message = 'Invalid app name format. The app path must use dot notation (e.g., "parent.child").'
55
+ raise CommandError(message)
56
+
57
+ def validate_app_path(self, components: list[str]) -> None:
58
+ """
59
+ Validates that an app path doesn't already exist.
60
+
61
+ :param components: List of app path components.
62
+ :raises CommandError: If an app already exists at the destination path.
63
+ """
64
+
65
+ destination = self._path_resolver.get_app_destination(components)
66
+
67
+ if self._filesystem.has_content(destination):
68
+ self._reporter.write('\n', self._reporter.style_notice)
69
+
70
+ message = (
71
+ f'The app already exists at {destination}. '
72
+ 'Please remove the existing app or choose a different name.'
73
+ )
74
+
75
+ raise CommandError(message)
76
+
77
+ def validate_root_app(self, components: list[str]) -> None:
78
+ """
79
+ Validates that the root app component is registered in Django.
80
+
81
+ :param components: List of app path components.
82
+ :raises CommandError: If the root app is not a valid registered app.
83
+ """
84
+
85
+ valid_roots = self._registry.get_valid_root_apps()
86
+ root = components[0]
87
+
88
+ if root not in valid_roots:
89
+ self._reporter.write('\n', self._reporter.style_notice)
90
+
91
+ message = (
92
+ f'Invalid root app "{root}". '
93
+ f'Valid root apps: {", ".join(sorted(valid_roots))}.'
94
+ )
95
+
96
+ raise CommandError(message)
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from django_spire.core.middleware.maintenance import MaintenanceMiddleware
4
- from django_spire.core.middleware.profiling import ProfilingMiddleware
5
4
 
6
5
 
7
- __all__ = ['MaintenanceMiddleware', 'ProfilingMiddleware']
6
+ __all__ = ['MaintenanceMiddleware']
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  # These imports are for the migrations to work as a single app "django_spire_knowledge"
2
4
 
3
5
  from django_spire.knowledge.collection.admin import *
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.apps import AppConfig
2
4
 
3
5
  from django_spire.utils import check_required_apps
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.auth.controller.controller import BaseAuthController
2
4
 
3
5
 
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
4
+
2
5
  from .models import Collection, CollectionGroup
3
6
 
4
7
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django import forms
2
4
 
3
5
  from django_spire.knowledge.collection.models import Collection
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.db import models
2
4
 
3
5
  from django_spire.auth.group.models import AuthGroup
@@ -44,10 +46,12 @@ class CollectionGroup(models.Model):
44
46
  related_name='groups',
45
47
  related_query_name='group',
46
48
  )
49
+
47
50
  auth_group = models.ForeignKey(
48
51
  AuthGroup,
49
52
  on_delete=models.CASCADE,
50
53
  related_name='collection_groups',
51
54
  related_query_name='collection_group',
52
55
  )
56
+
53
57
  services = CollectionGroupService()
@@ -2,15 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from django.core.handlers.wsgi import WSGIRequest
6
- from django.db.models import Count, Q
5
+ from django.db.models import Count
7
6
 
8
7
  from django_spire.auth.controller.controller import AppAuthController
9
8
  from django_spire.contrib.ordering.querysets import OrderingQuerySetMixin
10
9
  from django_spire.history.querysets import HistoryQuerySet
11
10
 
12
11
  if TYPE_CHECKING:
13
- from django_spire.auth.user.models import AuthUser
12
+ from django.core.handlers.wsgi import WSGIRequest
14
13
  from django.db.models import QuerySet
15
14
  from django_spire.knowledge.collection.models import Collection
16
15
 
@@ -57,6 +56,7 @@ class CollectionQuerySet(HistoryQuerySet, OrderingQuerySetMixin):
57
56
  accessible_ids = set(direct_access.values_list('id', flat=True))
58
57
 
59
58
  current_level_ids = accessible_ids.copy()
59
+
60
60
  while current_level_ids:
61
61
  next_level = self.filter(parent_id__in=current_level_ids)
62
62
  new_ids = set(next_level.values_list('id', flat=True)) - accessible_ids
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.collection.seeding.seeder import CollectionSeeder
2
4
 
3
5
  parent_collections = CollectionSeeder.seed_database(count=5)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.collection.models import Collection
2
4
 
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django.contrib.sites.models import Site
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django.core.handlers.wsgi import WSGIRequest
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.core.handlers.wsgi import WSGIRequest
2
4
  from django.shortcuts import get_object_or_404
3
5
  from django.template.response import TemplateResponse
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any
2
4
 
3
5
  from django.core.handlers.wsgi import WSGIRequest
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from django.contrib.contenttypes.models import ContentType
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from typing import TYPE_CHECKING
5
4
 
6
5
  from django.conf import settings
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.collection.tests.factories import create_test_collection
2
4
  from django_spire.knowledge.entry.models import Entry
3
5
 
@@ -9,5 +11,6 @@ def create_test_entry(**kwargs) -> Entry:
9
11
  'is_deleted': False,
10
12
  'is_active': True
11
13
  }
14
+
12
15
  data.update(kwargs)
13
16
  return Entry.objects.create(**data)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.urls import reverse
2
4
 
3
5
  from django_spire.core.tests.test_cases import BaseTestCase
@@ -2,6 +2,7 @@ from django.urls import path
2
2
 
3
3
  from django_spire.knowledge.entry.views import form_views
4
4
 
5
+
5
6
  app_name = 'form'
6
7
 
7
8
  urlpatterns = [
@@ -2,6 +2,7 @@ from django.urls import path
2
2
 
3
3
  from django_spire.knowledge.entry.views import json_views
4
4
 
5
+
5
6
  app_name = 'json'
6
7
 
7
8
  urlpatterns = [
@@ -2,6 +2,7 @@ from django.urls import path
2
2
 
3
3
  from django_spire.knowledge.entry.views import page_views
4
4
 
5
+
5
6
  app_name = 'page'
6
7
 
7
8
  urlpatterns = [
@@ -2,6 +2,7 @@ from django.urls import path
2
2
 
3
3
  from django_spire.knowledge.entry.views import template_views
4
4
 
5
+
5
6
  app_name = 'template'
6
7
 
7
8
  urlpatterns = [
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
 
3
5
  from django_spire.knowledge.entry.version.models import EntryVersion
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.contrib import admin
2
4
 
3
5
  from django_spire.knowledge.entry.version.block.models import EntryVersionBlock
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
2
4
  from django_spire.knowledge.entry.version.block.blocks.block import BaseBlock
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
2
4
  from django_spire.knowledge.entry.version.block.blocks.block import BaseBlock
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
2
4
  from django_spire.knowledge.entry.version.block.blocks.block import BaseBlock
3
5
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django_spire.knowledge.entry.version.block.blocks.list_block import ListItemBlock
2
4
  from django_spire.knowledge.entry.version.block.choices import BlockTypeChoices
3
5
  from django_spire.knowledge.entry.version.block.blocks.heading_block import HeadingBlock
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from django.db import models
2
4
  from django.forms import model_to_dict
3
5
  from django.template.loader import render_to_string
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django_spire.knowledge.entry.version.block.blocks.text_block import TextBlock
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django.urls import reverse
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
 
3
5
  from django.core.handlers.wsgi import WSGIRequest