django-spire 0.19.4__py3-none-any.whl → 0.20.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.
- django_spire/consts.py +1 -1
- django_spire/core/migrations/0001_initial.py +26 -0
- django_spire/core/migrations/__init__.py +0 -0
- django_spire/core/static/django_spire/css/app-background.css +6 -0
- django_spire/core/static/django_spire/css/app-import.css +1 -0
- django_spire/core/static/django_spire/css/app-override.css +0 -0
- django_spire/core/static/django_spire/css/themes/ayu/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/ayu/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/catppuccin/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/catppuccin/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/default/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/default/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/gruvbox/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/gruvbox/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/material/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/material/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/nord/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/nord/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/one-dark/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/one-dark/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/palenight/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/palenight/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/rose-pine/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/rose-pine/app-light.css +2 -0
- django_spire/core/static/django_spire/css/themes/tokyo-night/app-dark.css +2 -0
- django_spire/core/static/django_spire/css/themes/tokyo-night/app-light.css +2 -0
- django_spire/core/tags/__init__.py +0 -0
- django_spire/core/tags/intelligence/__init__.py +0 -0
- django_spire/core/tags/intelligence/tag_set_bot.py +41 -0
- django_spire/core/tags/mixins.py +61 -0
- django_spire/core/tags/models.py +38 -0
- django_spire/core/tags/querysets.py +9 -0
- django_spire/core/tags/tests/__init__.py +0 -0
- django_spire/core/tags/tests/test_intelligence.py +28 -0
- django_spire/core/tags/tests/test_tags.py +102 -0
- django_spire/core/tags/tools.py +20 -0
- django_spire/knowledge/collection/admin.py +20 -1
- django_spire/knowledge/collection/models.py +6 -1
- django_spire/knowledge/collection/services/service.py +2 -0
- django_spire/knowledge/collection/services/tag_service.py +52 -0
- django_spire/knowledge/collection/views/form_views.py +2 -0
- django_spire/knowledge/entry/admin.py +18 -2
- django_spire/knowledge/entry/models.py +6 -1
- django_spire/knowledge/entry/services/service.py +2 -0
- django_spire/knowledge/entry/services/tag_service.py +34 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +5 -1
- django_spire/knowledge/entry/version/views/json_views.py +9 -0
- django_spire/knowledge/intelligence/decoders/collection_decoder.py +4 -4
- django_spire/knowledge/intelligence/decoders/entry_decoder.py +2 -2
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +9 -3
- django_spire/knowledge/migrations/0008_collection_tags_entry_tags.py +24 -0
- django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +16 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/{detail_container.html → editor_container.html} +6 -0
- django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +27 -10
- django_spire/theme/enums.py +0 -3
- django_spire/theme/models.py +0 -3
- django_spire/theme/tests/test_context_processor.py +6 -6
- django_spire/theme/tests/test_enums.py +0 -3
- django_spire/theme/tests/test_integration.py +2 -2
- django_spire/theme/tests/test_model.py +23 -24
- django_spire/theme/tests/test_views/test_json_views.py +4 -4
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/METADATA +2 -2
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/RECORD +66 -56
- django_spire/core/static/django_spire/css/themes/dracula/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/dracula/app-light.css +0 -66
- django_spire/core/static/django_spire/css/themes/oceanic-next/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/oceanic-next/app-light.css +0 -66
- django_spire/core/static/django_spire/css/themes/synthwave/app-dark.css +0 -71
- django_spire/core/static/django_spire/css/themes/synthwave/app-light.css +0 -66
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/WHEEL +0 -0
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from dandy import Prompt
|
|
6
|
+
|
|
7
|
+
from django_spire.contrib.service import BaseDjangoModelService
|
|
8
|
+
from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from django_spire.knowledge.collection.models import Collection
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CollectionTagService(BaseDjangoModelService['Collection']):
|
|
15
|
+
obj: Collection
|
|
16
|
+
|
|
17
|
+
def process_and_set_tags(self):
|
|
18
|
+
collection_prompt = Prompt()
|
|
19
|
+
|
|
20
|
+
collection_prompt.sub_heading(self.obj.name)
|
|
21
|
+
collection_prompt.text(self.obj.description)
|
|
22
|
+
|
|
23
|
+
tag_set = TagSetBot().process(
|
|
24
|
+
content=collection_prompt
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
self.obj.set_tags_from_tag_set(
|
|
28
|
+
tag_set=tag_set,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def get_aggregated_tag_set(self):
|
|
32
|
+
tag_set = self.obj.tag_set
|
|
33
|
+
|
|
34
|
+
for collection in self.obj.children.active():
|
|
35
|
+
tag_set.update(collection.services.tag.get_aggregated_tag_set())
|
|
36
|
+
|
|
37
|
+
for entry in self.obj.entries.active():
|
|
38
|
+
tag_set.update(entry.tag_set)
|
|
39
|
+
|
|
40
|
+
return tag_set
|
|
41
|
+
|
|
42
|
+
def get_aggregated_tag_set_simplified(self):
|
|
43
|
+
tag_set = self.obj.tag_set_simplified
|
|
44
|
+
|
|
45
|
+
for collection in self.obj.children.active():
|
|
46
|
+
tag_set.update(collection.services.tag.get_aggregated_tag_set())
|
|
47
|
+
|
|
48
|
+
for entry in self.obj.entries.active():
|
|
49
|
+
tag_set.update(entry.tag_set_simplified)
|
|
50
|
+
|
|
51
|
+
return tag_set
|
|
52
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from django.contrib import admin
|
|
1
|
+
from django.contrib import admin, messages
|
|
2
|
+
from django.db.models import QuerySet
|
|
2
3
|
from django.urls import reverse
|
|
3
4
|
from django.utils.html import format_html
|
|
4
5
|
from django.utils.http import urlencode
|
|
@@ -8,12 +9,13 @@ from django_spire.knowledge.entry.models import Entry
|
|
|
8
9
|
|
|
9
10
|
@admin.register(Entry)
|
|
10
11
|
class EntryAdmin(admin.ModelAdmin):
|
|
11
|
-
list_display = ['name', 'current_version_link', 'collection', 'is_deleted']
|
|
12
|
+
list_display = ['name', 'current_version_link', 'collection', 'is_deleted', 'tag_count']
|
|
12
13
|
list_select_related = ['collection', 'current_version']
|
|
13
14
|
list_filter = ['is_deleted', 'is_active']
|
|
14
15
|
search_fields = ['name', 'collection__name']
|
|
15
16
|
ordering = ['name']
|
|
16
17
|
autocomplete_fields = ['collection', 'current_version']
|
|
18
|
+
actions = ['set_tags_for_entries']
|
|
17
19
|
|
|
18
20
|
def current_version_link(self, entry: Entry) -> str:
|
|
19
21
|
url = (
|
|
@@ -26,3 +28,17 @@ class EntryAdmin(admin.ModelAdmin):
|
|
|
26
28
|
|
|
27
29
|
current_version_link.short_description = 'Current Version'
|
|
28
30
|
current_version_link.allow_tags = True
|
|
31
|
+
|
|
32
|
+
@admin.action(description="Set Tags for Entries (Allow 5 Seconds Per)")
|
|
33
|
+
def set_tags_for_entries(self, request, queryset: QuerySet[Entry]):
|
|
34
|
+
processed = 0
|
|
35
|
+
for entry in queryset:
|
|
36
|
+
entry.services.tag.process_and_set_tags()
|
|
37
|
+
processed += 1
|
|
38
|
+
|
|
39
|
+
messages.success(request, f'Successfully processed {processed} entries.')
|
|
40
|
+
|
|
41
|
+
def tag_count(self, entry: Entry):
|
|
42
|
+
return entry.tags.count()
|
|
43
|
+
|
|
44
|
+
tag_count.short_description = 'Tags'
|
|
@@ -3,6 +3,7 @@ from django.urls import reverse
|
|
|
3
3
|
|
|
4
4
|
from django_spire.contrib import Breadcrumbs
|
|
5
5
|
from django_spire.contrib.ordering.mixins import OrderingModelMixin
|
|
6
|
+
from django_spire.core.tags.mixins import TagsModelMixin
|
|
6
7
|
from django_spire.contrib.utils import truncate_string
|
|
7
8
|
from django_spire.history.mixins import HistoryModelMixin
|
|
8
9
|
from django_spire.knowledge.collection.models import Collection
|
|
@@ -11,7 +12,11 @@ from django_spire.knowledge.entry.services.service import EntryService
|
|
|
11
12
|
from django_spire.knowledge.entry.version.models import EntryVersion
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class Entry(
|
|
15
|
+
class Entry(
|
|
16
|
+
HistoryModelMixin,
|
|
17
|
+
OrderingModelMixin,
|
|
18
|
+
TagsModelMixin
|
|
19
|
+
):
|
|
15
20
|
collection = models.ForeignKey(
|
|
16
21
|
Collection,
|
|
17
22
|
on_delete=models.CASCADE,
|
|
@@ -10,6 +10,7 @@ from django_spire.knowledge.entry.services.automation_service import \
|
|
|
10
10
|
from django_spire.knowledge.entry.services.factory_service import EntryFactoryService
|
|
11
11
|
from django_spire.knowledge.entry.services.processor_service import \
|
|
12
12
|
EntryProcessorService
|
|
13
|
+
from django_spire.knowledge.entry.services.tag_service import EntryTagService
|
|
13
14
|
from django_spire.knowledge.entry.services.tool_service import EntryToolService
|
|
14
15
|
from django_spire.knowledge.entry.services.transformation_services import \
|
|
15
16
|
EntryTransformationService
|
|
@@ -26,6 +27,7 @@ class EntryService(BaseDjangoModelService['Entry']):
|
|
|
26
27
|
factory = EntryFactoryService()
|
|
27
28
|
ordering = OrderingService()
|
|
28
29
|
processor = EntryProcessorService()
|
|
30
|
+
tag = EntryTagService()
|
|
29
31
|
tool = EntryToolService()
|
|
30
32
|
transformation = EntryTransformationService()
|
|
31
33
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from dandy import Prompt
|
|
6
|
+
|
|
7
|
+
from django_spire.contrib.service import BaseDjangoModelService
|
|
8
|
+
from django_spire.core.tags.intelligence.tag_set_bot import TagSetBot
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from django_spire.knowledge.entry.models import Entry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EntryTagService(BaseDjangoModelService['Entry']):
|
|
16
|
+
obj: Entry
|
|
17
|
+
|
|
18
|
+
def process_and_set_tags(self):
|
|
19
|
+
entry_prompt = Prompt()
|
|
20
|
+
|
|
21
|
+
entry_prompt.text(self.obj.name)
|
|
22
|
+
|
|
23
|
+
for version_block in self.obj.current_version.blocks.all():
|
|
24
|
+
entry_prompt.text(f'{version_block.render_to_text()}')
|
|
25
|
+
|
|
26
|
+
tag_set = TagSetBot().process(
|
|
27
|
+
content=entry_prompt
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.obj.set_tags_from_tag_set(
|
|
31
|
+
tag_set=tag_set,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
@@ -9,8 +9,12 @@ app_name = 'json'
|
|
|
9
9
|
urlpatterns = [
|
|
10
10
|
path(
|
|
11
11
|
'<int:pk>/update_blocks/',
|
|
12
|
-
|
|
13
12
|
json_views.update_blocks_view,
|
|
14
13
|
name='update_blocks',
|
|
15
14
|
),
|
|
15
|
+
path(
|
|
16
|
+
'<int:pk>/update_entry_from_version/',
|
|
17
|
+
json_views.update_entry_from_version_view,
|
|
18
|
+
name='update_entry_from_version',
|
|
19
|
+
),
|
|
16
20
|
]
|
|
@@ -21,3 +21,12 @@ def update_blocks_view(request: WSGIRequest, pk: int) -> JsonResponse:
|
|
|
21
21
|
entry_version.services.processor.add_update_delete_blocks(block_data_list)
|
|
22
22
|
|
|
23
23
|
return JsonResponse({'type': 'success'})
|
|
24
|
+
|
|
25
|
+
@valid_ajax_request_required
|
|
26
|
+
@AppAuthController('knowledge').permission_required('can_change')
|
|
27
|
+
def update_entry_from_version_view(request: WSGIRequest, pk: int) -> JsonResponse:
|
|
28
|
+
entry_version = get_object_or_404(EntryVersion, pk=pk)
|
|
29
|
+
|
|
30
|
+
entry_version.entry.services.tag.process_and_set_tags()
|
|
31
|
+
|
|
32
|
+
return JsonResponse({'type': 'success'})
|
|
@@ -7,13 +7,13 @@ from django_spire.knowledge.collection.models import Collection
|
|
|
7
7
|
|
|
8
8
|
def get_collection_decoder() -> Decoder:
|
|
9
9
|
class CollectionDecoder(Decoder):
|
|
10
|
-
mapping_keys_description = 'Knowledge
|
|
10
|
+
mapping_keys_description = 'Knowledge Collections Tags'
|
|
11
11
|
mapping = {
|
|
12
12
|
**{
|
|
13
|
-
f'{collection.
|
|
13
|
+
f'{collection.services.tag.get_aggregated_tag_set_simplified()}': collection
|
|
14
14
|
for collection in Collection.objects.all().annotate_entry_count()
|
|
15
15
|
},
|
|
16
|
-
'No Matching Knowledge Collection
|
|
16
|
+
'No Matching Knowledge Collection Tags': None,
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
return CollectionDecoder()
|
|
19
|
+
return CollectionDecoder()
|
|
@@ -7,10 +7,10 @@ from django_spire.knowledge.collection.models import Collection
|
|
|
7
7
|
|
|
8
8
|
def get_entry_decoder(collection: Collection) -> Decoder:
|
|
9
9
|
class EntryDecoder(Decoder):
|
|
10
|
-
mapping_keys_description = 'Knowledge Entries'
|
|
10
|
+
mapping_keys_description = 'Knowledge Entries Tags'
|
|
11
11
|
mapping = {
|
|
12
12
|
**{
|
|
13
|
-
entry.
|
|
13
|
+
f'{entry.tag_set}': entry
|
|
14
14
|
for entry in collection.entries.all()
|
|
15
15
|
},
|
|
16
16
|
'No Matching Knowledge Entries': None
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from django.core.handlers.wsgi import WSGIRequest
|
|
6
|
+
|
|
7
|
+
from django_spire.ai.chat.message_intel import DefaultMessageIntel, BaseMessageIntel
|
|
6
8
|
from django_spire.knowledge.intelligence.bots.entry_search_llm_bot import EntrySearchBot
|
|
7
9
|
from django_spire.knowledge.intelligence.intel.entry_intel import EntriesIntel
|
|
8
10
|
from django_spire.knowledge.intelligence.intel.message_intel import KnowledgeMessageIntel
|
|
@@ -13,17 +15,21 @@ if TYPE_CHECKING:
|
|
|
13
15
|
from django.core.handlers.wsgi import WSGIRequest
|
|
14
16
|
from dandy.llm.request.message import MessageHistory
|
|
15
17
|
|
|
18
|
+
NO_KNOWLEDGE_MESSAGE_INTEL = DefaultMessageIntel(
|
|
19
|
+
text='Sorry, I could not find any information on that.'
|
|
20
|
+
)
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
def knowledge_search_workflow(
|
|
18
24
|
request: WSGIRequest,
|
|
19
25
|
user_input: str,
|
|
20
26
|
message_history: MessageHistory,
|
|
21
|
-
) ->
|
|
27
|
+
) -> BaseMessageIntel | None:
|
|
22
28
|
collection_decoder = get_collection_decoder()
|
|
23
29
|
collections = collection_decoder.process(user_input).values
|
|
24
30
|
|
|
25
31
|
if collections[0] is None:
|
|
26
|
-
return
|
|
32
|
+
return NO_KNOWLEDGE_MESSAGE_INTEL
|
|
27
33
|
|
|
28
34
|
entries = []
|
|
29
35
|
|
|
@@ -36,7 +42,7 @@ def knowledge_search_workflow(
|
|
|
36
42
|
entries = [entry for entry in entries if entry is not None]
|
|
37
43
|
|
|
38
44
|
if not entries:
|
|
39
|
-
return
|
|
45
|
+
return NO_KNOWLEDGE_MESSAGE_INTEL
|
|
40
46
|
|
|
41
47
|
entries_intel = EntriesIntel(entry_intel_list=[])
|
|
42
48
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 5.2.7 on 2025-11-12 16:48
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('django_spire_core', '0001_initial'),
|
|
10
|
+
('django_spire_knowledge', '0007_alter_collection_options'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='collection',
|
|
16
|
+
name='tags',
|
|
17
|
+
field=models.ManyToManyField(blank=True, editable=False, null=True, related_name='+', to='django_spire_core.tag'),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='entry',
|
|
21
|
+
name='tags',
|
|
22
|
+
field=models.ManyToManyField(blank=True, editable=False, null=True, related_name='+', to='django_spire_core.tag'),
|
|
23
|
+
),
|
|
24
|
+
]
|
|
@@ -46,4 +46,20 @@
|
|
|
46
46
|
Last Edits ETC
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
|
+
<div class="row">
|
|
50
|
+
<div class="col-12 col-lg-8 mx-auto pb-4">
|
|
51
|
+
Tags:
|
|
52
|
+
{% for tag in collection.tags.all %}
|
|
53
|
+
<span class="badge bg-secondary">{{ tag.name }}</span>
|
|
54
|
+
{% endfor %}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="row">
|
|
58
|
+
<div class="col-12 col-lg-8 mx-auto pb-4">
|
|
59
|
+
Aggregated Tags:
|
|
60
|
+
{% for tag in collection.services.tag.get_aggregated_tag_set %}
|
|
61
|
+
<span class="badge bg-secondary">{{ tag }}</span>
|
|
62
|
+
{% endfor %}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
49
65
|
{% endblock %}
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
KNOWLEDGE_ENTRY_VERSION_EDITOR?.readOnly?.toggle()
|
|
22
22
|
this.editor_is_readonly = KNOWLEDGE_ENTRY_VERSION_EDITOR?.readOnly?.isEnabled
|
|
23
23
|
|
|
24
|
+
if (this.editor_is_readonly) {
|
|
25
|
+
django_glue_fetch(
|
|
26
|
+
'{% url "django_spire:knowledge:entry:version:json:update_entry_from_version" pk=entry.id %}',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
let view_mode = this.editor_is_readonly ? 'readonly' : 'edit'
|
|
25
31
|
|
|
26
32
|
let url = new URL(window.location)
|
|
@@ -16,32 +16,47 @@
|
|
|
16
16
|
{% endblock %}
|
|
17
17
|
|
|
18
18
|
{% block full_page_info_navigation %}
|
|
19
|
-
<div>
|
|
20
|
-
<div class="mb-3">
|
|
19
|
+
<div class="row">
|
|
20
|
+
<div class="col-12 mb-3">
|
|
21
21
|
{% include 'django_spire/element/attribute_element.html' with attribute_title='Author' attribute_value=entry.current_version.author.get_full_name %}
|
|
22
22
|
</div>
|
|
23
|
+
</div>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
<div class="row">
|
|
26
|
+
<div class="col-12 mb-3">
|
|
25
27
|
{% include 'django_spire/element/attribute_element.html' with attribute_title='Status' %}
|
|
26
28
|
{% include 'django_spire/knowledge/entry/badge/entry_status_badge.html' with status=entry.current_version.status %}
|
|
27
29
|
</div>
|
|
30
|
+
</div>
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
{% if entry.current_version.published_datetime %}
|
|
33
|
+
<div class="row">
|
|
34
|
+
<div class="col-12 mb-3">
|
|
31
35
|
{% include 'django_spire/element/attribute_element.html' with attribute_title='Published' attribute_value=entry.current_version.published_datetime %}
|
|
32
36
|
</div>
|
|
33
|
-
|
|
37
|
+
</div>
|
|
38
|
+
{% endif %}
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
<div class="row">
|
|
41
|
+
<div class="col-12 mb-3">
|
|
36
42
|
{% include 'django_spire/element/attribute_element.html' with attribute_title='Last Edit' attribute_value=entry.current_version.last_edit_datetime %}
|
|
37
43
|
</div>
|
|
38
44
|
</div>
|
|
45
|
+
|
|
46
|
+
<div class="row">
|
|
47
|
+
<div class="col-12 mb-3">
|
|
48
|
+
{% include 'django_spire/element/attribute_element.html' with attribute_title='Tags' %}
|
|
49
|
+
{% for tag in entry.tags.all %}
|
|
50
|
+
<span class="badge bg-secondary">{{ tag.name }}</span>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
39
54
|
{% endblock %}
|
|
40
55
|
|
|
41
56
|
{% block knowledge_full_page_content %}
|
|
42
57
|
<div class="row justify-content-center ps-3">
|
|
43
58
|
<div class="col-12">
|
|
44
|
-
{% include 'django_spire/knowledge/entry/version/container/
|
|
59
|
+
{% include 'django_spire/knowledge/entry/version/container/editor_container.html' %}
|
|
45
60
|
</div>
|
|
46
61
|
</div>
|
|
47
62
|
{% endblock %}
|
|
@@ -49,6 +64,8 @@
|
|
|
49
64
|
{% block base_body_additional_bottom_js %}
|
|
50
65
|
{{ block.super }}
|
|
51
66
|
|
|
52
|
-
<script src="{% static 'django_spire/knowledge/entry/version/js/null_paragraph.js' %}?v={{ DJANGO_SPIRE_VERSION }}"
|
|
53
|
-
|
|
67
|
+
<script src="{% static 'django_spire/knowledge/entry/version/js/null_paragraph.js' %}?v={{ DJANGO_SPIRE_VERSION }}"
|
|
68
|
+
type="application/javascript"></script>
|
|
69
|
+
<script src="{% static 'django_spire/knowledge/entry/version/js/editor.js' %}?v={{ DJANGO_SPIRE_VERSION }}"
|
|
70
|
+
type="application/javascript"></script>
|
|
54
71
|
{% endblock %}
|
django_spire/theme/enums.py
CHANGED
|
@@ -7,15 +7,12 @@ class ThemeFamily(StrEnum):
|
|
|
7
7
|
AYU = 'ayu'
|
|
8
8
|
CATPPUCCIN = 'catppuccin'
|
|
9
9
|
DEFAULT = 'default'
|
|
10
|
-
DRACULA = 'dracula'
|
|
11
10
|
GRUVBOX = 'gruvbox'
|
|
12
11
|
MATERIAL = 'material'
|
|
13
12
|
NORD = 'nord'
|
|
14
|
-
OCEANIC_NEXT = 'oceanic-next'
|
|
15
13
|
ONE_DARK = 'one-dark'
|
|
16
14
|
PALENIGHT = 'palenight'
|
|
17
15
|
ROSE_PINE = 'rose-pine'
|
|
18
|
-
SYNTHWAVE = 'synthwave'
|
|
19
16
|
TOKYO_NIGHT = 'tokyo-night'
|
|
20
17
|
|
|
21
18
|
|
django_spire/theme/models.py
CHANGED
|
@@ -19,15 +19,12 @@ class Theme:
|
|
|
19
19
|
ThemeFamily.AYU: 'Ayu',
|
|
20
20
|
ThemeFamily.CATPPUCCIN: 'Catppuccin',
|
|
21
21
|
ThemeFamily.DEFAULT: 'Default',
|
|
22
|
-
ThemeFamily.DRACULA: 'Dracula',
|
|
23
22
|
ThemeFamily.GRUVBOX: 'Gruvbox',
|
|
24
23
|
ThemeFamily.MATERIAL: 'Material',
|
|
25
24
|
ThemeFamily.NORD: 'Nord',
|
|
26
|
-
ThemeFamily.OCEANIC_NEXT: 'Oceanic Next',
|
|
27
25
|
ThemeFamily.ONE_DARK: 'One Dark Pro',
|
|
28
26
|
ThemeFamily.PALENIGHT: 'Palenight',
|
|
29
27
|
ThemeFamily.ROSE_PINE: 'Rose Pine',
|
|
30
|
-
ThemeFamily.SYNTHWAVE: 'Synthwave',
|
|
31
28
|
ThemeFamily.TOKYO_NIGHT: 'Tokyo Night',
|
|
32
29
|
}
|
|
33
30
|
|
|
@@ -18,14 +18,14 @@ class ThemeContextProcessorTests(TestCase):
|
|
|
18
18
|
request = self.factory.get('/')
|
|
19
19
|
request.COOKIES = {}
|
|
20
20
|
|
|
21
|
-
with patch('django_spire.conf.settings.DJANGO_SPIRE_DEFAULT_THEME', '
|
|
21
|
+
with patch('django_spire.conf.settings.DJANGO_SPIRE_DEFAULT_THEME', 'gruvbox-dark'):
|
|
22
22
|
context = theme_context(request)
|
|
23
23
|
|
|
24
|
-
self.assertEqual(context['DJANGO_SPIRE_DEFAULT_THEME'], '
|
|
24
|
+
self.assertEqual(context['DJANGO_SPIRE_DEFAULT_THEME'], 'gruvbox-dark')
|
|
25
25
|
self.assertIn('theme', context)
|
|
26
26
|
|
|
27
27
|
data = context['theme']
|
|
28
|
-
self.assertEqual(data['family'], '
|
|
28
|
+
self.assertEqual(data['family'], 'gruvbox')
|
|
29
29
|
self.assertEqual(data['mode'], 'dark')
|
|
30
30
|
|
|
31
31
|
def test_theme_context_with_cookie(self) -> None:
|
|
@@ -45,11 +45,11 @@ class ThemeContextProcessorTests(TestCase):
|
|
|
45
45
|
cookie_name = get_theme_cookie_name()
|
|
46
46
|
request.COOKIES = {cookie_name: 'invalid-theme'}
|
|
47
47
|
|
|
48
|
-
with patch('django_spire.conf.settings.DJANGO_SPIRE_DEFAULT_THEME', '
|
|
48
|
+
with patch('django_spire.conf.settings.DJANGO_SPIRE_DEFAULT_THEME', 'gruvbox-dark'):
|
|
49
49
|
context = theme_context(request)
|
|
50
50
|
|
|
51
51
|
data = context['theme']
|
|
52
|
-
self.assertEqual(data['family'], '
|
|
52
|
+
self.assertEqual(data['family'], 'gruvbox')
|
|
53
53
|
self.assertEqual(data['mode'], 'dark')
|
|
54
54
|
|
|
55
55
|
def test_theme_context_path_setting(self) -> None:
|
|
@@ -70,7 +70,7 @@ class ThemeContextProcessorIntegrationTests(BaseTestCase):
|
|
|
70
70
|
|
|
71
71
|
def test_theme_context_with_authenticated_client(self) -> None:
|
|
72
72
|
cookie_name = get_theme_cookie_name()
|
|
73
|
-
self.client.cookies[cookie_name] = '
|
|
73
|
+
self.client.cookies[cookie_name] = 'gruvbox-dark'
|
|
74
74
|
response = self.client.get('/')
|
|
75
75
|
self.assertEqual(response.status_code, 200)
|
|
76
76
|
|
|
@@ -40,9 +40,9 @@ class ThemeIntegrationTests(BaseTestCase):
|
|
|
40
40
|
self.assertEqual(response.status_code, 200)
|
|
41
41
|
|
|
42
42
|
def test_theme_media_integration(self) -> None:
|
|
43
|
-
theme = Theme(family=ThemeFamily.
|
|
43
|
+
theme = Theme(family=ThemeFamily.GRUVBOX, mode='dark')
|
|
44
44
|
stylesheet = theme.stylesheet
|
|
45
45
|
|
|
46
46
|
self.assertIn('django_spire/css/themes/', stylesheet)
|
|
47
|
-
self.assertIn('
|
|
47
|
+
self.assertIn('gruvbox', stylesheet)
|
|
48
48
|
self.assertIn('app-dark.css', stylesheet)
|
|
@@ -27,13 +27,13 @@ class ThemeModelTests(TestCase):
|
|
|
27
27
|
self.assertIn(family, Theme.FAMILY_DISPLAY_NAMES)
|
|
28
28
|
|
|
29
29
|
def test_theme_initialization_with_enums(self) -> None:
|
|
30
|
-
theme = Theme(family=ThemeFamily.
|
|
31
|
-
self.assertEqual(theme.family, ThemeFamily.
|
|
30
|
+
theme = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
31
|
+
self.assertEqual(theme.family, ThemeFamily.GRUVBOX)
|
|
32
32
|
self.assertEqual(theme.mode, ThemeMode.DARK)
|
|
33
33
|
|
|
34
34
|
def test_theme_initialization_with_strings(self) -> None:
|
|
35
|
-
theme = Theme(family='
|
|
36
|
-
self.assertEqual(theme.family, ThemeFamily.
|
|
35
|
+
theme = Theme(family='gruvbox', mode='dark')
|
|
36
|
+
self.assertEqual(theme.family, ThemeFamily.GRUVBOX)
|
|
37
37
|
self.assertEqual(theme.mode, ThemeMode.DARK)
|
|
38
38
|
|
|
39
39
|
def test_theme_initialization_invalid_family(self) -> None:
|
|
@@ -43,16 +43,15 @@ class ThemeModelTests(TestCase):
|
|
|
43
43
|
|
|
44
44
|
def test_theme_initialization_invalid_mode(self) -> None:
|
|
45
45
|
with pytest.raises(ValueError) as ctx:
|
|
46
|
-
Theme(family='
|
|
46
|
+
Theme(family='gruvbox', mode='invalid-mode')
|
|
47
47
|
self.assertIn('Invalid theme mode', str(ctx.value))
|
|
48
48
|
|
|
49
49
|
def test_from_string_valid(self) -> None:
|
|
50
50
|
cases = [
|
|
51
51
|
('default-light', ThemeFamily.DEFAULT, ThemeMode.LIGHT),
|
|
52
|
-
('
|
|
52
|
+
('gruvbox-dark', ThemeFamily.GRUVBOX, ThemeMode.DARK),
|
|
53
53
|
('one-dark-light', ThemeFamily.ONE_DARK, ThemeMode.LIGHT),
|
|
54
54
|
('tokyo-night-dark', ThemeFamily.TOKYO_NIGHT, ThemeMode.DARK),
|
|
55
|
-
('oceanic-next-light', ThemeFamily.OCEANIC_NEXT, ThemeMode.LIGHT),
|
|
56
55
|
]
|
|
57
56
|
|
|
58
57
|
for string, family, mode in cases:
|
|
@@ -62,7 +61,7 @@ class ThemeModelTests(TestCase):
|
|
|
62
61
|
self.assertEqual(theme.mode, mode)
|
|
63
62
|
|
|
64
63
|
def test_from_string_empty_with_default(self) -> None:
|
|
65
|
-
default = Theme(family=ThemeFamily.
|
|
64
|
+
default = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
66
65
|
theme = Theme.from_string('', default=default)
|
|
67
66
|
self.assertEqual(theme, default)
|
|
68
67
|
|
|
@@ -72,7 +71,7 @@ class ThemeModelTests(TestCase):
|
|
|
72
71
|
self.assertEqual(theme.mode, Theme.DEFAULT_MODE)
|
|
73
72
|
|
|
74
73
|
def test_from_string_invalid_with_default(self) -> None:
|
|
75
|
-
default = Theme(family=ThemeFamily.
|
|
74
|
+
default = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
76
75
|
theme = Theme.from_string('invalid', default=default)
|
|
77
76
|
self.assertEqual(theme, default)
|
|
78
77
|
|
|
@@ -97,27 +96,27 @@ class ThemeModelTests(TestCase):
|
|
|
97
96
|
self.assertEqual(default.mode, Theme.DEFAULT_MODE)
|
|
98
97
|
|
|
99
98
|
def test_display_property(self) -> None:
|
|
100
|
-
theme = Theme(family=ThemeFamily.
|
|
101
|
-
self.assertEqual(theme.display, '
|
|
99
|
+
theme = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
100
|
+
self.assertEqual(theme.display, 'Gruvbox - Dark')
|
|
102
101
|
|
|
103
102
|
theme = Theme(family=ThemeFamily.ONE_DARK, mode=ThemeMode.LIGHT)
|
|
104
103
|
self.assertEqual(theme.display, 'One Dark Pro - Light')
|
|
105
104
|
|
|
106
105
|
def test_family_display_property(self) -> None:
|
|
107
|
-
theme = Theme(family=ThemeFamily.
|
|
108
|
-
self.assertEqual(theme.family_display, '
|
|
106
|
+
theme = Theme(family=ThemeFamily.ONE_DARK, mode=ThemeMode.DARK)
|
|
107
|
+
self.assertEqual(theme.family_display, 'One Dark Pro')
|
|
109
108
|
|
|
110
109
|
def test_is_dark_property(self) -> None:
|
|
111
|
-
dark = Theme(family=ThemeFamily.
|
|
110
|
+
dark = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
112
111
|
self.assertTrue(dark.is_dark)
|
|
113
112
|
|
|
114
|
-
light = Theme(family=ThemeFamily.
|
|
113
|
+
light = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.LIGHT)
|
|
115
114
|
self.assertFalse(light.is_dark)
|
|
116
115
|
|
|
117
116
|
def test_stylesheet_property(self) -> None:
|
|
118
117
|
cases = [
|
|
119
118
|
(ThemeFamily.DEFAULT, ThemeMode.LIGHT, 'django_spire/css/themes/default/app-light.css'),
|
|
120
|
-
(ThemeFamily.
|
|
119
|
+
(ThemeFamily.GRUVBOX, ThemeMode.DARK, 'django_spire/css/themes/gruvbox/app-dark.css'),
|
|
121
120
|
(ThemeFamily.ONE_DARK, ThemeMode.LIGHT, 'django_spire/css/themes/one-dark/app-light.css'),
|
|
122
121
|
(ThemeFamily.TOKYO_NIGHT, ThemeMode.DARK, 'django_spire/css/themes/tokyo-night/app-dark.css'),
|
|
123
122
|
]
|
|
@@ -130,7 +129,7 @@ class ThemeModelTests(TestCase):
|
|
|
130
129
|
def test_value_property(self) -> None:
|
|
131
130
|
cases = [
|
|
132
131
|
(ThemeFamily.DEFAULT, ThemeMode.LIGHT, 'default-light'),
|
|
133
|
-
(ThemeFamily.
|
|
132
|
+
(ThemeFamily.GRUVBOX, ThemeMode.DARK, 'gruvbox-dark'),
|
|
134
133
|
(ThemeFamily.ONE_DARK, ThemeMode.LIGHT, 'one-dark-light'),
|
|
135
134
|
(ThemeFamily.TOKYO_NIGHT, ThemeMode.DARK, 'tokyo-night-dark'),
|
|
136
135
|
]
|
|
@@ -141,7 +140,7 @@ class ThemeModelTests(TestCase):
|
|
|
141
140
|
self.assertEqual(theme.value, value)
|
|
142
141
|
|
|
143
142
|
def test_to_dict(self) -> None:
|
|
144
|
-
theme = Theme(family=ThemeFamily.
|
|
143
|
+
theme = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
145
144
|
result = theme.to_dict()
|
|
146
145
|
|
|
147
146
|
keys = {
|
|
@@ -156,16 +155,16 @@ class ThemeModelTests(TestCase):
|
|
|
156
155
|
|
|
157
156
|
self.assertEqual(set(result.keys()), keys)
|
|
158
157
|
|
|
159
|
-
self.assertEqual(result['display'], '
|
|
160
|
-
self.assertEqual(result['family'], '
|
|
161
|
-
self.assertEqual(result['family_display'], '
|
|
162
|
-
self.assertEqual(result['full'], '
|
|
158
|
+
self.assertEqual(result['display'], 'Gruvbox - Dark')
|
|
159
|
+
self.assertEqual(result['family'], 'gruvbox')
|
|
160
|
+
self.assertEqual(result['family_display'], 'Gruvbox')
|
|
161
|
+
self.assertEqual(result['full'], 'gruvbox-dark')
|
|
163
162
|
self.assertTrue(result['is_dark'])
|
|
164
163
|
self.assertEqual(result['mode'], 'dark')
|
|
165
|
-
self.assertEqual(result['stylesheet'], 'django_spire/css/themes/
|
|
164
|
+
self.assertEqual(result['stylesheet'], 'django_spire/css/themes/gruvbox/app-dark.css')
|
|
166
165
|
|
|
167
166
|
def test_theme_immutability(self) -> None:
|
|
168
|
-
theme = Theme(family=ThemeFamily.
|
|
167
|
+
theme = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
|
|
169
168
|
|
|
170
169
|
with pytest.raises(AttributeError):
|
|
171
170
|
theme.family = ThemeFamily.DEFAULT
|