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.
Files changed (72) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/core/migrations/0001_initial.py +26 -0
  3. django_spire/core/migrations/__init__.py +0 -0
  4. django_spire/core/static/django_spire/css/app-background.css +6 -0
  5. django_spire/core/static/django_spire/css/app-import.css +1 -0
  6. django_spire/core/static/django_spire/css/app-override.css +0 -0
  7. django_spire/core/static/django_spire/css/themes/ayu/app-dark.css +2 -0
  8. django_spire/core/static/django_spire/css/themes/ayu/app-light.css +2 -0
  9. django_spire/core/static/django_spire/css/themes/catppuccin/app-dark.css +2 -0
  10. django_spire/core/static/django_spire/css/themes/catppuccin/app-light.css +2 -0
  11. django_spire/core/static/django_spire/css/themes/default/app-dark.css +2 -0
  12. django_spire/core/static/django_spire/css/themes/default/app-light.css +2 -0
  13. django_spire/core/static/django_spire/css/themes/gruvbox/app-dark.css +2 -0
  14. django_spire/core/static/django_spire/css/themes/gruvbox/app-light.css +2 -0
  15. django_spire/core/static/django_spire/css/themes/material/app-dark.css +2 -0
  16. django_spire/core/static/django_spire/css/themes/material/app-light.css +2 -0
  17. django_spire/core/static/django_spire/css/themes/nord/app-dark.css +2 -0
  18. django_spire/core/static/django_spire/css/themes/nord/app-light.css +2 -0
  19. django_spire/core/static/django_spire/css/themes/one-dark/app-dark.css +2 -0
  20. django_spire/core/static/django_spire/css/themes/one-dark/app-light.css +2 -0
  21. django_spire/core/static/django_spire/css/themes/palenight/app-dark.css +2 -0
  22. django_spire/core/static/django_spire/css/themes/palenight/app-light.css +2 -0
  23. django_spire/core/static/django_spire/css/themes/rose-pine/app-dark.css +2 -0
  24. django_spire/core/static/django_spire/css/themes/rose-pine/app-light.css +2 -0
  25. django_spire/core/static/django_spire/css/themes/tokyo-night/app-dark.css +2 -0
  26. django_spire/core/static/django_spire/css/themes/tokyo-night/app-light.css +2 -0
  27. django_spire/core/tags/__init__.py +0 -0
  28. django_spire/core/tags/intelligence/__init__.py +0 -0
  29. django_spire/core/tags/intelligence/tag_set_bot.py +41 -0
  30. django_spire/core/tags/mixins.py +61 -0
  31. django_spire/core/tags/models.py +38 -0
  32. django_spire/core/tags/querysets.py +9 -0
  33. django_spire/core/tags/tests/__init__.py +0 -0
  34. django_spire/core/tags/tests/test_intelligence.py +28 -0
  35. django_spire/core/tags/tests/test_tags.py +102 -0
  36. django_spire/core/tags/tools.py +20 -0
  37. django_spire/knowledge/collection/admin.py +20 -1
  38. django_spire/knowledge/collection/models.py +6 -1
  39. django_spire/knowledge/collection/services/service.py +2 -0
  40. django_spire/knowledge/collection/services/tag_service.py +52 -0
  41. django_spire/knowledge/collection/views/form_views.py +2 -0
  42. django_spire/knowledge/entry/admin.py +18 -2
  43. django_spire/knowledge/entry/models.py +6 -1
  44. django_spire/knowledge/entry/services/service.py +2 -0
  45. django_spire/knowledge/entry/services/tag_service.py +34 -0
  46. django_spire/knowledge/entry/version/urls/json_urls.py +5 -1
  47. django_spire/knowledge/entry/version/views/json_views.py +9 -0
  48. django_spire/knowledge/intelligence/decoders/collection_decoder.py +4 -4
  49. django_spire/knowledge/intelligence/decoders/entry_decoder.py +2 -2
  50. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +9 -3
  51. django_spire/knowledge/migrations/0008_collection_tags_entry_tags.py +24 -0
  52. django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +16 -0
  53. django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/{detail_container.html → editor_container.html} +6 -0
  54. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +27 -10
  55. django_spire/theme/enums.py +0 -3
  56. django_spire/theme/models.py +0 -3
  57. django_spire/theme/tests/test_context_processor.py +6 -6
  58. django_spire/theme/tests/test_enums.py +0 -3
  59. django_spire/theme/tests/test_integration.py +2 -2
  60. django_spire/theme/tests/test_model.py +23 -24
  61. django_spire/theme/tests/test_views/test_json_views.py +4 -4
  62. {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/METADATA +2 -2
  63. {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/RECORD +66 -56
  64. django_spire/core/static/django_spire/css/themes/dracula/app-dark.css +0 -71
  65. django_spire/core/static/django_spire/css/themes/dracula/app-light.css +0 -66
  66. django_spire/core/static/django_spire/css/themes/oceanic-next/app-dark.css +0 -71
  67. django_spire/core/static/django_spire/css/themes/oceanic-next/app-light.css +0 -66
  68. django_spire/core/static/django_spire/css/themes/synthwave/app-dark.css +0 -71
  69. django_spire/core/static/django_spire/css/themes/synthwave/app-light.css +0 -66
  70. {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/WHEEL +0 -0
  71. {django_spire-0.19.4.dist-info → django_spire-0.20.0.dist-info}/licenses/LICENSE.md +0 -0
  72. {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
+
@@ -61,6 +61,8 @@ def form_view(
61
61
  collection=collection,
62
62
  )
63
63
 
64
+ collection.services.tag.process_and_set_tags()
65
+
64
66
  if collection.parent_id:
65
67
  return_url = reverse(
66
68
  'django_spire:knowledge:collection:page:top_level',
@@ -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(HistoryModelMixin, OrderingModelMixin):
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 Collection Titles'
10
+ mapping_keys_description = 'Knowledge Collections Tags'
11
11
  mapping = {
12
12
  **{
13
- f'{collection.name}: {collection.description}': 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 Titles': None,
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.name: 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
- ) -> KnowledgeMessageIntel | None:
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 None
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 None
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
- <div class="mb-3">
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
- {% if entry.current_version.published_datetime %}
30
- <div class="mb-3">
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
- {% endif %}
37
+ </div>
38
+ {% endif %}
34
39
 
35
- <div class="mb-3">
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/detail_container.html' %}
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 }}" type="application/javascript"></script>
53
- <script src="{% static 'django_spire/knowledge/entry/version/js/editor.js' %}?v={{ DJANGO_SPIRE_VERSION }}" type="application/javascript"></script>
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 %}
@@ -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
 
@@ -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', 'dracula-dark'):
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'], 'dracula-dark')
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'], 'dracula')
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', 'dracula-dark'):
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'], 'dracula')
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] = 'dracula-dark'
73
+ self.client.cookies[cookie_name] = 'gruvbox-dark'
74
74
  response = self.client.get('/')
75
75
  self.assertEqual(response.status_code, 200)
76
76
 
@@ -11,15 +11,12 @@ class ThemeEnumTests(TestCase):
11
11
  'ayu',
12
12
  'catppuccin',
13
13
  'default',
14
- 'dracula',
15
14
  'gruvbox',
16
15
  'material',
17
16
  'nord',
18
- 'oceanic-next',
19
17
  'one-dark',
20
18
  'palenight',
21
19
  'rose-pine',
22
- 'synthwave',
23
20
  'tokyo-night'
24
21
  }
25
22
 
@@ -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.DRACULA, mode='dark')
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('dracula', stylesheet)
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.DRACULA, mode=ThemeMode.DARK)
31
- self.assertEqual(theme.family, ThemeFamily.DRACULA)
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='dracula', mode='dark')
36
- self.assertEqual(theme.family, ThemeFamily.DRACULA)
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='dracula', mode='invalid-mode')
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
- ('dracula-dark', ThemeFamily.DRACULA, ThemeMode.DARK),
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.DRACULA, mode=ThemeMode.DARK)
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.DRACULA, mode=ThemeMode.DARK)
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.DRACULA, mode=ThemeMode.DARK)
101
- self.assertEqual(theme.display, 'Dracula - Dark')
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.OCEANIC_NEXT, mode=ThemeMode.DARK)
108
- self.assertEqual(theme.family_display, 'Oceanic Next')
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.DRACULA, mode=ThemeMode.DARK)
110
+ dark = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
112
111
  self.assertTrue(dark.is_dark)
113
112
 
114
- light = Theme(family=ThemeFamily.DRACULA, mode=ThemeMode.LIGHT)
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.DRACULA, ThemeMode.DARK, 'django_spire/css/themes/dracula/app-dark.css'),
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.DRACULA, ThemeMode.DARK, 'dracula-dark'),
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.DRACULA, mode=ThemeMode.DARK)
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'], 'Dracula - Dark')
160
- self.assertEqual(result['family'], 'dracula')
161
- self.assertEqual(result['family_display'], 'Dracula')
162
- self.assertEqual(result['full'], 'dracula-dark')
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/dracula/app-dark.css')
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.DRACULA, mode=ThemeMode.DARK)
167
+ theme = Theme(family=ThemeFamily.GRUVBOX, mode=ThemeMode.DARK)
169
168
 
170
169
  with pytest.raises(AttributeError):
171
170
  theme.family = ThemeFamily.DEFAULT